Browse Source

Merge pull request #12520 from cakephp/psr16-wrapper

RFC - Implement a PSR16 bridge for existing Cache adapters
Mark Story 7 years ago
parent
commit
7f33677e88

+ 1 - 0
composer.json

@@ -34,6 +34,7 @@
         "cakephp/chronos": "^1.0.1",
         "aura/intl": "^3.0.0",
         "psr/log": "^1.0.0",
+        "psr/simple-cache": "^1.0.0",
         "zendframework/zend-diactoros": "^1.4.0"
     },
     "suggest": {

+ 11 - 0
src/Cache/Cache.php

@@ -235,6 +235,17 @@ class Cache
     }
 
     /**
+     * Get a SimpleCacheEngine object for the named cache pool.
+     *
+     * @param string $config The name of the configured cache backend.
+     * @return \Cake\Cache\SimpleCacheEngine
+     */
+    public static function pool($config)
+    {
+        return new SimpleCacheEngine(static::engine($config));
+    }
+
+    /**
      * Garbage collection
      *
      * Permanently remove all expired and deleted data

+ 25 - 0
src/Cache/InvalidArgumentException.php

@@ -0,0 +1,25 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Cache;
+
+use Cake\Core\Exception\Exception;
+use Psr\SimpleCache\InvalidArgumentException as InvalidArgumentInterface;
+
+/**
+ * Exception raised when cache keys are invalid.
+ */
+class InvalidArgumentException extends Exception implements InvalidArgumentInterface
+{
+}

+ 240 - 0
src/Cache/SimpleCacheEngine.php

@@ -0,0 +1,240 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace Cake\Cache;
+
+use Psr\SimpleCache\CacheInterface;
+
+/**
+ * Wrapper for Cake engines that allow them to support
+ * the PSR16 Simple Cache Interface
+ *
+ * @since 3.7.0
+ * @link https://www.php-fig.org/psr/psr-16/
+ */
+class SimpleCacheEngine implements CacheInterface
+{
+    /**
+     * The wrapped cache engine object.
+     *
+     * @var \Cake\Cache\CacheEngine
+     */
+    protected $innerEngine;
+
+    /**
+     * Constructor
+     *
+     * @param \Cake\Cache\CacheEngine $innerEngine The decorated engine.
+     */
+    public function __construct($innerEngine)
+    {
+        $this->innerEngine = $innerEngine;
+    }
+
+    /**
+     * Ensure the validity of the given cache key.
+     *
+     * @param string $key Key to check.
+     * @return void
+     * @throws \Cake\Cache\InvalidArgumentException When the key is not valid.
+     */
+    protected function ensureValidKey($key)
+    {
+        if (!is_string($key) || strlen($key) === 0) {
+            throw new InvalidArgumentException('A cache key must be a non-empty string.');
+        }
+    }
+
+    /**
+     * Ensure the validity of the given cache keys.
+     *
+     * @param mixed $keys The keys to check.
+     * @return void
+     * @throws \Cake\Cache\InvalidArgumentException When the keys are not valid.
+     */
+    protected function ensureValidKeys($keys)
+    {
+        if (!is_array($keys) && !($keys instanceof \Traversable)) {
+            throw new InvalidArgumentException('A cache key set must be either an array or a Traversable.');
+        }
+
+        foreach ($keys as $key) {
+            $this->ensureValidKey($key);
+        }
+    }
+
+    /**
+     * Fetches the value for a given key from the cache.
+     *
+     * @param string $key The unique key of this item in the cache.
+     * @param mixed $default Default value to return if the key does not exist.
+     * @return mixed The value of the item from the cache, or $default in case of cache miss.
+     * @throws \Cake\Cache\InvalidArgumentException If the $key string is not a legal value.
+     */
+    public function get($key, $default = null)
+    {
+        $this->ensureValidKey($key);
+        $result = $this->innerEngine->read($key);
+        if ($result === false) {
+            return $default;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Persists data in the cache, uniquely referenced by the given key with an optional expiration TTL time.
+     *
+     * @param string $key The key of the item to store.
+     * @param mixed $value The value of the item to store, must be serializable.
+     * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
+     *   the driver supports TTL then the library may set a default value
+     *   for it or let the driver take care of that.
+     * @return bool True on success and false on failure.
+     * @throws \Cake\Cache\InvalidArgumentException
+     *   MUST be thrown if the $key string is not a legal value.
+     */
+    public function set($key, $value, $ttl = null)
+    {
+        $this->ensureValidKey($key);
+        if ($ttl !== null) {
+            $restore = $this->innerEngine->getConfig('duration');
+            $this->innerEngine->setConfig('duration', $ttl);
+        }
+        try {
+            $result = $this->innerEngine->write($key, $value);
+
+            return (bool)$result;
+        } finally {
+            if (isset($restore)) {
+                $this->innerEngine->setConfig('duration', $restore);
+            }
+        }
+    }
+
+    /**
+     * Delete an item from the cache by its unique key.
+     *
+     * @param string $key The unique cache key of the item to delete.
+     * @return bool True if the item was successfully removed. False if there was an error.
+     * @throws \Cake\Cache\InvalidArgumentException If the $key string is not a legal value.
+     */
+    public function delete($key)
+    {
+        $this->ensureValidKey($key);
+
+        return $this->innerEngine->delete($key);
+    }
+
+    /**
+     * Wipes clean the entire cache's keys.
+     *
+     * @return bool True on success and false on failure.
+     */
+    public function clear()
+    {
+        return $this->innerEngine->clear(false);
+    }
+
+    /**
+     * Obtains multiple cache items by their unique keys.
+     *
+     * @param iterable $keys A list of keys that can obtained in a single operation.
+     * @param mixed $default Default value to return for keys that do not exist.
+     * @return iterable A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
+     * @throws \Cake\Cache\InvalidArgumentException If $keys is neither an array nor a Traversable,
+     *   or if any of the $keys are not a legal value.
+     */
+    public function getMultiple($keys, $default = null)
+    {
+        $this->ensureValidKeys($keys);
+
+        $results = $this->innerEngine->readMany($keys);
+        foreach ($results as $key => $value) {
+            if ($value === false) {
+                $results[$key] = $default;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Persists a set of key => value pairs in the cache, with an optional TTL.
+     *
+     * @param iterable $values A list of key => value pairs for a multiple-set operation.
+     * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
+     *   the driver supports TTL then the library may set a default value
+     *   for it or let the driver take care of that.
+     * @return bool True on success and false on failure.
+     * @throws \Cake\Cache\InvalidArgumentException If $values is neither an array nor a Traversable,
+     *   or if any of the $values are not a legal value.
+     */
+    public function setMultiple($values, $ttl = null)
+    {
+        $this->ensureValidKeys(array_keys($values));
+
+        if ($ttl !== null) {
+            $restore = $this->innerEngine->getConfig('duration');
+            $this->innerEngine->setConfig('duration', $ttl);
+        }
+        try {
+            return $this->innerEngine->writeMany($values);
+        } finally {
+            if (isset($restore)) {
+                $this->innerEngine->setConfig('duration', $restore);
+            }
+        }
+    }
+
+    /**
+     * Deletes multiple cache items in a single operation.
+     *
+     * @param iterable $keys A list of string-based keys to be deleted.
+     * @return bool True if the items were successfully removed. False if there was an error.
+     * @throws \Cake\Cache\InvalidArgumentException If $keys is neither an array nor a Traversable,
+     *   or if any of the $keys are not a legal value.
+     */
+    public function deleteMultiple($keys)
+    {
+        $this->ensureValidKeys($keys);
+
+        $result = $this->innerEngine->deleteMany($keys);
+        foreach ($result as $key => $success) {
+            if ($success === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Determines whether an item is present in the cache.
+     *
+     * NOTE: It is recommended that has() is only to be used for cache warming type purposes
+     * and not to be used within your live applications operations for get/set, as this method
+     * is subject to a race condition where your has() will return true and immediately after,
+     * another script can remove it making the state of your app out of date.
+     *
+     * @param string $key The cache item key.
+     * @return bool
+     * @throws \Cake\Cache\InvalidArgumentException If the $key string is not a legal value.
+     */
+    public function has($key)
+    {
+        return $this->get($key) !== null;
+    }
+}

+ 26 - 0
tests/TestCase/Cache/CacheTest.php

@@ -22,6 +22,7 @@ use Cake\Core\Plugin;
 use Cake\TestSuite\TestCase;
 use InvalidArgumentException;
 use PHPUnit\Framework\Error\Error;
+use Psr\SimpleCache\CacheInterface as SimpleCacheInterface;
 
 /**
  * CacheTest class
@@ -907,4 +908,29 @@ class CacheTest extends TestCase
 
         $this->assertSame($registry, Cache::getRegistry());
     }
+
+    /**
+     * Test getting instances with pool
+     *
+     * @return void
+     */
+    public function testPool()
+    {
+        $this->_configCache();
+
+        $pool = Cache::pool('tests');
+        $this->assertInstanceOf(SimpleCacheInterface::class, $pool);
+    }
+
+    /**
+     * Test getting instances with pool
+     *
+     * @return void
+     */
+    public function testPoolCacheDisabled()
+    {
+        Cache::disable();
+        $pool = Cache::pool('tests');
+        $this->assertInstanceOf(SimpleCacheInterface::class, $pool);
+    }
 }

+ 435 - 0
tests/TestCase/Cache/SimpleCacheEngineTest.php

@@ -0,0 +1,435 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace Cake\Test\TestCase\Cache;
+
+use Cake\Cache\CacheEngine;
+use Cake\Cache\Engine\FileEngine;
+use Cake\Cache\SimpleCacheEngine;
+use Cake\TestSuite\TestCase;
+use Psr\SimpleCache\InvalidArgumentException;
+
+/**
+ * SimpleCacheEngine class
+ *
+ * @coversDefaultClass \Cake\Cache\SimpleCacheEngine
+ */
+class SimpleCacheEngineTest extends TestCase
+{
+    /**
+     * The inner cache engine
+     *
+     * @var CacheEngine
+     */
+    protected $innerEngine;
+
+    /**
+     * The simple cache engine under test
+     *
+     * @var SimpleCacheEngine
+     */
+    protected $cache;
+
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->innerEngine = new FileEngine();
+        $this->innerEngine->init([
+            'prefix' => '',
+            'path' => TMP . 'tests',
+            'duration' => 5,
+        ]);
+        $this->cache = new SimpleCacheEngine($this->innerEngine);
+    }
+
+    /**
+     * Tear down
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        $this->innerEngine->clear(false);
+    }
+
+    /**
+     * Test getting keys
+     *
+     * @return void
+     * @covers ::get
+     * @covers ::__construct
+     * @covers ::ensureValidKey
+     */
+    public function testGetSuccess()
+    {
+        $this->innerEngine->write('key_one', 'Some Value');
+        $this->assertSame('Some Value', $this->cache->get('key_one'));
+        $this->assertSame('Some Value', $this->cache->get('key_one', 'default'));
+    }
+
+    /**
+     * Test get on missing keys
+     *
+     * @return void
+     * @covers ::get
+     */
+    public function testGetNoKey()
+    {
+        $this->assertSame('default', $this->cache->get('no', 'default'));
+        $this->assertNull($this->cache->get('no'));
+    }
+
+    /**
+     * Test get on invalid keys. The PSR spec outlines that an exception
+     * must be raised.
+     *
+     * @return void
+     * @covers ::get
+     * @covers ::ensureValidKey
+     */
+    public function testGetInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $this->cache->get('');
+    }
+
+    /**
+     * Test set() inheriting the default TTL
+     *
+     * @return void
+     * @covers ::set
+     * @covers ::__construct
+     */
+    public function testSetNoTtl()
+    {
+        $this->assertTrue($this->cache->set('key', 'a value'));
+        $this->assertSame('a value', $this->cache->get('key'));
+    }
+
+    /**
+     * Test the TTL parameter of set()
+     *
+     * @return void
+     * @covers ::set
+     */
+    public function testSetWithTtl()
+    {
+        $this->assertTrue($this->cache->set('key', 'a value'));
+        $ttl = 0;
+        $this->assertTrue($this->cache->set('expired', 'a value', $ttl));
+
+        sleep(1);
+        $this->assertSame('a value', $this->cache->get('key'));
+        $this->assertNull($this->cache->get('expired'));
+        $this->assertSame(5, $this->innerEngine->getConfig('duration'));
+    }
+
+    /**
+     * Test set() with an invalid key.
+     *
+     * @return void
+     * @covers ::set
+     * @covers ::ensureValidKey
+     */
+    public function testSetInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $this->cache->set('', 'some data');
+    }
+
+    /**
+     * Test delete on known and unknown keys
+     *
+     * @return void
+     * @covers ::delete
+     */
+    public function testDelete()
+    {
+        $this->cache->set('key', 'a value');
+        $this->assertTrue($this->cache->delete('key'));
+        $this->assertFalse($this->cache->delete('undefined'));
+    }
+
+    /**
+     * Test delete on an invalid key
+     *
+     * @return void
+     * @covers ::delete
+     * @covers ::ensureValidKey
+     */
+    public function testDeleteInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $this->cache->delete('');
+    }
+
+    /**
+     * Test clearing cache data
+     *
+     * @return void
+     * @covers ::clear
+     */
+    public function testClear()
+    {
+        $this->cache->set('key', 'a value');
+        $this->cache->set('key2', 'other value');
+
+        $this->assertTrue($this->cache->clear());
+        $this->assertNull($this->cache->get('key'));
+        $this->assertNull($this->cache->get('key2'));
+    }
+
+    /**
+     * Test getMultiple
+     *
+     * @return void
+     * @covers ::getMultiple
+     */
+    public function testGetMultiple()
+    {
+        $this->cache->set('key', 'a value');
+        $this->cache->set('key2', 'other value');
+
+        $results = $this->cache->getMultiple(['key', 'key2', 'no']);
+        $expected = [
+            'key' => 'a value',
+            'key2' => 'other value',
+            'no' => null,
+        ];
+        $this->assertSame($expected, $results);
+    }
+
+    /**
+     * Test getting multiple keys with an invalid key
+     *
+     * @return void
+     * @covers ::getMultiple
+     * @covers ::ensureValidKeys
+     * @covers ::ensureValidKey
+     */
+    public function testGetMultipleInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $withInvalidKey = [''];
+        $this->cache->getMultiple($withInvalidKey);
+    }
+
+    /**
+     * Test getting multiple keys with an invalid keys parameter
+     *
+     * @return void
+     * @covers ::getMultiple
+     * @covers ::ensureValidKeys
+     */
+    public function testGetMultipleInvalidKeys()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key set must be either an array or a Traversable.');
+        $notAnArray = 'neither an array nor a Traversable';
+        $this->cache->getMultiple($notAnArray);
+    }
+
+    /**
+     * Test getMultiple adding defaults in.
+     *
+     * @return void
+     * @covers ::getMultiple
+     */
+    public function testGetMultipleDefault()
+    {
+        $this->cache->set('key', 'a value');
+        $this->cache->set('key2', 'other value');
+
+        $results = $this->cache->getMultiple(['key', 'key2', 'no'], 'default value');
+        $expected = [
+            'key' => 'a value',
+            'key2' => 'other value',
+            'no' => 'default value',
+        ];
+        $this->assertSame($expected, $results);
+    }
+
+    /**
+     * Test setMultiple
+     *
+     * We should not assert for array equality, as the PSR-16 specs
+     * do not make any guarantees on key order.
+     *
+     * @return void
+     * @covers ::setMultiple
+     */
+    public function testSetMultiple()
+    {
+        $data = [
+            'key' => 'a value',
+            'key2' => 'other value',
+        ];
+        $expected = [
+            'key2' => 'other value',
+            'key' => 'a value',
+        ];
+        $this->cache->setMultiple($data);
+
+        $results = $this->cache->getMultiple(array_keys($data));
+        $this->assertEquals($expected, $results);
+    }
+
+    /**
+     * Test setMultiple with an invalid key
+     *
+     * @return void
+     * @covers ::setMultiple
+     * @covers ::ensureValidKeys
+     * @covers ::ensureValidKey
+     */
+    public function testSetMultipleInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $data = [
+            '' => 'a value wuth an invalid key',
+        ];
+        $this->cache->setMultiple($data);
+    }
+
+    /**
+     * Test setMultiple with TTL parameter
+     *
+     * @return void
+     * @covers ::setMultiple
+     * @covers ::ensureValidKeys
+     */
+    public function testSetMultipleWithTtl()
+    {
+        $data = [
+            'key' => 'a value',
+            'key2' => 'other value',
+        ];
+        $ttl = 0;
+        $this->cache->setMultiple($data, $ttl);
+
+        sleep(1);
+        $results = $this->cache->getMultiple(array_keys($data));
+        $this->assertNull($results['key']);
+        $this->assertNull($results['key2']);
+        $this->assertSame(5, $this->innerEngine->getConfig('duration'));
+    }
+
+    /**
+     * Test deleting multiple keys
+     *
+     * @return void
+     * @covers ::deleteMultiple
+     */
+    public function testDeleteMultiple()
+    {
+        $data = [
+            'key' => 'a value',
+            'key2' => 'other value',
+            'key3' => 'more data',
+        ];
+        $this->cache->setMultiple($data);
+        $this->assertTrue($this->cache->deleteMultiple(['key', 'key3']));
+        $this->assertNull($this->cache->get('key'));
+        $this->assertNull($this->cache->get('key3'));
+        $this->assertSame('other value', $this->cache->get('key2'));
+    }
+
+    /**
+     * Test deleting multiple keys with an invalid key
+     *
+     * @return void
+     * @covers ::deleteMultiple
+     * @covers ::ensureValidKeys
+     * @covers ::ensureValidKey
+     */
+    public function testDeleteMultipleInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $withInvalidKey = [''];
+        $this->cache->deleteMultiple($withInvalidKey);
+    }
+
+    /**
+     * Test deleting multiple keys with an invalid keys parameter
+     *
+     * @return void
+     * @covers ::deleteMultiple
+     * @covers ::ensureValidKeys
+     */
+    public function testDeleteMultipleInvalidKeys()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key set must be either an array or a Traversable.');
+        $notAnArray = 'neither an array nor a Traversable';
+        $this->cache->deleteMultiple($notAnArray);
+    }
+
+    /**
+     * Test partial success with deleteMultiple
+     *
+     * @return void
+     * @covers ::deleteMultiple
+     */
+    public function testDeleteMultipleSomeMisses()
+    {
+        $data = [
+            'key' => 'a value',
+        ];
+        $this->cache->setMultiple($data);
+        $this->assertFalse($this->cache->deleteMultiple(['key', 'key3']));
+    }
+
+    /**
+     * Test has
+     *
+     * @return void
+     * @covers ::has
+     */
+    public function testHas()
+    {
+        $this->assertFalse($this->cache->has('key'));
+
+        $this->cache->set('key', 'value');
+        $this->assertTrue($this->cache->has('key'));
+    }
+
+    /**
+     * Test has with invalid key
+     *
+     * @return void
+     * @covers ::has
+     * @covers ::ensureValidKey
+     */
+    public function testHasInvalidKey()
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('A cache key must be a non-empty string.');
+        $this->cache->has('');
+    }
+}