From 9e4f9fd0c95fb4da9278eceab3dec0dabb4edb41 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Sun, 13 Jun 2021 23:37:10 -0500 Subject: [PATCH 1/7] New mock() test helper and expanded MockCache with assertions. --- system/Cache/CacheFactory.php | 17 +++- system/Exceptions/TestException.php | 25 ++++++ system/Helpers/test_helper.php | 31 +++++++ system/Language/en/Test.php | 15 ++++ system/Test/Mock/MockCache.php | 98 +++++++++++++++++------ tests/system/Cache/CacheMockTest.php | 39 +++++++++ user_guide_src/source/testing/index.rst | 3 +- user_guide_src/source/testing/mocking.rst | 53 ++++++++++++ 8 files changed, 255 insertions(+), 26 deletions(-) create mode 100644 system/Exceptions/TestException.php create mode 100644 system/Language/en/Test.php create mode 100644 tests/system/Cache/CacheMockTest.php create mode 100644 user_guide_src/source/testing/mocking.rst diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index e77961313827..897096330220 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -13,6 +13,7 @@ use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\Exceptions\CriticalError; +use CodeIgniter\Test\Mock\MockCache; use Config\Cache; /** @@ -22,6 +23,20 @@ */ class CacheFactory { + /** + * The class to use when mocking + * + * @var string + */ + public static $mockClass = MockCache::class; + + /** + * The service to inject the mock as + * + * @var string + */ + public static $mockServiceName = 'cache'; + /** * Attempts to create the desired cache handler, based upon the * @@ -83,6 +98,4 @@ public static function getHandler(Cache $config, string $handler = null, string return $adapter; } - - //-------------------------------------------------------------------- } diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php new file mode 100644 index 000000000000..fc270f2aa24d --- /dev/null +++ b/system/Exceptions/TestException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception for automatic logging. + */ +class TestException extends CriticalError +{ + use DebugTraceableTrait; + + public static function forInvalidMockClass(string $name) + { + return new static(lang('Test.invalidMockClass', [$name])); + } +} diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index c6f9bebe7577..5788e70027f1 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -9,7 +9,9 @@ * file that was distributed with this source code. */ +use CodeIgniter\Exceptions\TestException; use CodeIgniter\Test\Fabricator; +use Config\Services; /** * CodeIgniter Test Helpers @@ -45,3 +47,32 @@ function fake($model, array $overrides = null, $persist = true) return $fabricator->make(); } } + +if (! function_exists('mock')) +{ + /** + * Used within our test suite to mock certain system tools. + * All tools using this MUST use the MockableTrait + * + * @param string $className Fully qualified class name + */ + function mock(string $className) + { + $mockClass = $className::$mockClass; + $mockService = $className::$mockServiceName; + + if (empty($mockClass) || ! class_exists($mockClass)) + { + throw TestException::forInvalidMockClass($mockClass); + } + + $mock = new $mockClass(); + + if (! empty($mockService)) + { + Services::injectMock($mockService, $mock); + } + + return $mock; + } +} diff --git a/system/Language/en/Test.php b/system/Language/en/Test.php new file mode 100644 index 000000000000..8df1eb8c2f8c --- /dev/null +++ b/system/Language/en/Test.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Testing language settings +return [ + 'invalidMockClass' => '{0} is not a valid Mock class', +]; diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index f4fb1a972c5d..b8f05760a3a6 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -13,6 +13,7 @@ use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Cache\Handlers\BaseHandler; +use PHPUnit\Framework\Assert; use Closure; class MockCache extends BaseHandler implements CacheInterface @@ -31,7 +32,12 @@ class MockCache extends BaseHandler implements CacheInterface */ protected $expirations = []; - //-------------------------------------------------------------------- + /** + * If true, will not cache any data. + * + * @var boolean + */ + protected $bypass = false; /** * Takes care of any handler-specific setup that must be done. @@ -40,8 +46,6 @@ public function initialize() { } - //-------------------------------------------------------------------- - /** * Attempts to fetch an item from the cache store. * @@ -58,8 +62,6 @@ public function get(string $key) : null; } - //-------------------------------------------------------------------- - /** * Get an item from the cache, or execute the given Closure and store the result. * @@ -83,8 +85,6 @@ public function remember(string $key, int $ttl, Closure $callback) return $value; } - //-------------------------------------------------------------------- - /** * Saves an item to the cache store. * @@ -100,6 +100,11 @@ public function remember(string $key, int $ttl, Closure $callback) */ public function save(string $key, $value, int $ttl = 60, bool $raw = false) { + if ($this->bypass) + { + return false; + } + $key = static::validateKey($key, $this->prefix); $this->cache[$key] = $value; @@ -108,8 +113,6 @@ public function save(string $key, $value, int $ttl = 60, bool $raw = false) return true; } - //-------------------------------------------------------------------- - /** * Deletes a specific item from the cache store. * @@ -132,8 +135,6 @@ public function delete(string $key) return true; } - //-------------------------------------------------------------------- - /** * Deletes items from the cache store matching a given pattern. * @@ -157,8 +158,6 @@ public function deleteMatching(string $pattern) return $count; } - //-------------------------------------------------------------------- - /** * Performs atomic incrementation of a raw stored value. * @@ -184,8 +183,6 @@ public function increment(string $key, int $offset = 1) return $this->save($key, $data + $offset); } - //-------------------------------------------------------------------- - /** * Performs atomic decrementation of a raw stored value. * @@ -212,8 +209,6 @@ public function decrement(string $key, int $offset = 1) return $this->save($key, $data - $offset); } - //-------------------------------------------------------------------- - /** * Will delete all items in the entire cache. * @@ -227,8 +222,6 @@ public function clean() return true; } - //-------------------------------------------------------------------- - /** * Returns information on the entire cache. * @@ -242,8 +235,6 @@ public function getCacheInfo() return array_keys($this->cache); } - //-------------------------------------------------------------------- - /** * Returns detailed information about the specific item in the cache. * @@ -272,8 +263,6 @@ public function getMetaData(string $key) ]; } - //-------------------------------------------------------------------- - /** * Determines if the driver is supported on this system. * @@ -284,6 +273,69 @@ public function isSupported(): bool return true; } + //-------------------------------------------------------------------- + // Test Helpers //-------------------------------------------------------------------- + /** + * Instructs the class to ignore all + * requests to cache an item, and always "miss" + * when checked for existing data. + * + * @return $this + */ + public function bypass(bool $bypass = true) + { + $this->clean(); + + $this->bypass = $bypass; + + return $this; + } + + //-------------------------------------------------------------------- + // Additional Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the cache has an item named $key. + * The value is not checked since storing false or null + * values is valid. + * + * @param string $key + */ + public function assertHas(string $key) + { + Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`"); + } + + /** + * Asserts that the cache has an item named $key with a value matching $value. + * + * @param string $key + * @param null $value + */ + public function assertHasValue(string $key, $value = null) + { + $item = $this->get($key); + + // Let assertHas handle throwing the error for consistency + // if the key is not found + if (empty($item)) + { + $this->assertHas($key); + } + + Assert::assertEquals($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); + } + + /** + * Asserts that the cache does NOT have an item named $key. + * + * @param string $key + */ + public function assertMissing(string $key) + { + Assert::assertFalse(array_key_exists($key, $this->cache), "The cached item named `{$key}` exists."); + } } diff --git a/tests/system/Cache/CacheMockTest.php b/tests/system/Cache/CacheMockTest.php new file mode 100644 index 000000000000..7112f5bf972b --- /dev/null +++ b/tests/system/Cache/CacheMockTest.php @@ -0,0 +1,39 @@ +assertInstanceOf(BaseHandler::class, service('cache')); + + $mock = mock(CacheFactory::class); + + // Should return MockCache class + $this->assertInstanceOf(MockCache::class, $mock); + + // Should inject MockCache + $this->assertInstanceOf(MockCache::class, service('cache')); + } + + public function testMockCaching() + { + $mock = mock(CacheFactory::class); + + // Ensure it stores the value normally + $mock->save('foo', 'bar'); + $mock->assertHas('foo'); + $mock->assertHasValue('foo', 'bar'); + + // Try it again with bypass on + $mock->bypass(); + $mock->save('foo', 'bar'); + $mock->assertMissing('foo'); + } +} diff --git a/user_guide_src/source/testing/index.rst b/user_guide_src/source/testing/index.rst index 878cc9918355..949a68b99b51 100644 --- a/user_guide_src/source/testing/index.rst +++ b/user_guide_src/source/testing/index.rst @@ -2,7 +2,7 @@ Testing ####### -CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. +CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. The following sections should get you quickly testing your applications. .. toctree:: @@ -16,3 +16,4 @@ The following sections should get you quickly testing your applications. response benchmark debugging + Mocking diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst new file mode 100644 index 000000000000..ea0a30922dd4 --- /dev/null +++ b/user_guide_src/source/testing/mocking.rst @@ -0,0 +1,53 @@ +###################### +Mocking System Classes +###################### + +Several classes within the framework provide mocked versions of the classes that can be used during testing. These classes +can take the place of the normal class during test execution, often providing additional assertions to test that actions +have taken place (or not taken place) during the execution of the test. This might be checking data gets cached correctly, +emails were sent correctly, etc. + +.. contents:: + :local: + :depth: 1 + +Cache +===== + +You can mock the cache with the ``mock()`` method, using the ``CacheFactory`` as its only parameter. +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + +While this returns an instance of ``CodeIgniter\Test\Mock\MockCache`` that you can use directly, it also inserts the +mock into the Service class, so any calls within your code to ``service('cache')`` or ``Config\Services::cache()`` will +use the mocked class within its place. + +When using this in more than one test method within a single file you should call either the ``clean()`` or ``bypass()`` +methods during the test ``setUp()`` to ensure a clean slate when your tests run. + +Additional Methods +------------------ + +You can instruct the mocked cache handler to never do any caching with the ``bypass()`` method. This will emulate +using the dummy handler and ensures that your test does not rely on cached data for your tests. +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + // Never cache any items during this test. + $mock->bypass(); + +Available Assertions +-------------------- + +The following new assertions are available on the mocked class for using during testing: +:: + + $mock = mock(CodeIgniter\Cache\CacheFactory::class); + + // Assert that a cached item named $key exists + $mock->assertHas($key); + // Assert that a cached item named $key exists with a value of $value + $mock->assertHasValue($key, $value); + // Assert that a cached item named $key does NOT exist + $mock->assertMissing($key); From e019a7fee9df83bad8cf7434634d2c8eb32366ee Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 14 Jun 2021 08:13:33 -0500 Subject: [PATCH 2/7] Update system/Helpers/test_helper.php Co-authored-by: MGatner --- system/Helpers/test_helper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index 5788e70027f1..d33f9681b0d7 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -52,7 +52,6 @@ function fake($model, array $overrides = null, $persist = true) { /** * Used within our test suite to mock certain system tools. - * All tools using this MUST use the MockableTrait * * @param string $className Fully qualified class name */ From 55966e8fb433d2d1c2252f5224107e785b82fd2a Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 14 Jun 2021 08:13:52 -0500 Subject: [PATCH 3/7] Update system/Helpers/test_helper.php Co-authored-by: MGatner --- system/Helpers/test_helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index d33f9681b0d7..013ee9ff88aa 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -58,7 +58,7 @@ function fake($model, array $overrides = null, $persist = true) function mock(string $className) { $mockClass = $className::$mockClass; - $mockService = $className::$mockServiceName; + $mockService = $className::$mockServiceName ?? ''; if (empty($mockClass) || ! class_exists($mockClass)) { From a2fa932611a6406fc5196f26883277437662dd09 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 14 Jun 2021 08:17:05 -0500 Subject: [PATCH 4/7] Update user_guide_src/source/testing/mocking.rst Co-authored-by: MGatner --- user_guide_src/source/testing/mocking.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst index ea0a30922dd4..03b464cf6570 100644 --- a/user_guide_src/source/testing/mocking.rst +++ b/user_guide_src/source/testing/mocking.rst @@ -2,7 +2,7 @@ Mocking System Classes ###################### -Several classes within the framework provide mocked versions of the classes that can be used during testing. These classes +Several components within the framework provide mocked versions of their classes that can be used during testing. These classes can take the place of the normal class during test execution, often providing additional assertions to test that actions have taken place (or not taken place) during the execution of the test. This might be checking data gets cached correctly, emails were sent correctly, etc. From 5eafc2f20e51c06405b8fa97374fb151f300d8d9 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 16 Jun 2021 22:30:50 -0500 Subject: [PATCH 5/7] Updated assert to assertArrayNotHasKey per suggestion. --- system/Test/Mock/MockCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index b8f05760a3a6..d4c5657c375f 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -336,6 +336,6 @@ public function assertHasValue(string $key, $value = null) */ public function assertMissing(string $key) { - Assert::assertFalse(array_key_exists($key, $this->cache), "The cached item named `{$key}` exists."); + Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists."); } } From 463596f4169872949e723252f65e59905d540ac2 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 16 Jun 2021 22:41:40 -0500 Subject: [PATCH 6/7] Remove extra use statement in MockCache --- system/Test/Mock/MockCache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index b981d03901dc..10187b1cba56 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -15,7 +15,6 @@ use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Cache\Handlers\BaseHandler; use PHPUnit\Framework\Assert; -use Closure; class MockCache extends BaseHandler implements CacheInterface { From cd0d36af5cbd1e65aaba0fa9074973b452ea7911 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 18 Jun 2021 22:31:49 -0500 Subject: [PATCH 7/7] Replaced code that went missing during merge conflict fixes. --- system/Test/Mock/MockCache.php | 618 +++++++++++++++++---------------- 1 file changed, 317 insertions(+), 301 deletions(-) diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index 10187b1cba56..42e84103e823 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -18,305 +18,321 @@ class MockCache extends BaseHandler implements CacheInterface { - /** - * Mock cache storage. - * - * @var array - */ - protected $cache = []; - - /** - * Expiration times. - * - * @var ?int[] - */ - protected $expirations = []; - - /** - * If true, will not cache any data. - * - * @var boolean - */ - protected $bypass = false; - - /** - * Takes care of any handler-specific setup that must be done. - */ - public function initialize() - { - } - - /** - * Attempts to fetch an item from the cache store. - * - * @param string $key Cache item name - * - * @return mixed - */ - public function get(string $key) - { - $key = static::validateKey($key, $this->prefix); - - return $this->cache[$key] ?? null; - } - - /** - * Get an item from the cache, or execute the given Closure and store the result. - * - * @param string $key Cache item name - * @param int $ttl Time to live - * @param Closure $callback Callback return value - * - * @return mixed - */ - public function remember(string $key, int $ttl, Closure $callback) - { - $value = $this->get($key); - - if (! is_null($value)) { - return $value; - } - - $this->save($key, $value = $callback(), $ttl); - - return $value; - } - - /** - * Saves an item to the cache store. - * - * The $raw parameter is only utilized by Mamcache in order to - * allow usage of increment() and decrement(). - * - * @param string $key Cache item name - * @param mixed $value the data to save - * @param int $ttl Time To Live, in seconds (default 60) - * @param bool $raw Whether to store the raw value. - * - * @return bool - */ - public function save(string $key, $value, int $ttl = 60, bool $raw = false) - { - $key = static::validateKey($key, $this->prefix); - - $this->cache[$key] = $value; - $this->expirations[$key] = $ttl > 0 ? time() + $ttl : null; - - return true; - } - - /** - * Deletes a specific item from the cache store. - * - * @param string $key Cache item name - * - * @return bool - */ - public function delete(string $key) - { - $key = static::validateKey($key, $this->prefix); - - if (! isset($this->cache[$key])) { - return false; - } - - unset($this->cache[$key], $this->expirations[$key]); - - return true; - } - - /** - * Deletes items from the cache store matching a given pattern. - * - * @param string $pattern Cache items glob-style pattern - * - * @return int - */ - public function deleteMatching(string $pattern) - { - $count = 0; - - foreach (array_keys($this->cache) as $key) { - if (fnmatch($pattern, $key)) { - $count++; - unset($this->cache[$key], $this->expirations[$key]); - } - } - - return $count; - } - - /** - * Performs atomic incrementation of a raw stored value. - * - * @param string $key Cache ID - * @param int $offset Step/value to increase by - * - * @return bool - */ - public function increment(string $key, int $offset = 1) - { - $key = static::validateKey($key, $this->prefix); - $data = $this->cache[$key] ?: null; - - if (empty($data)) { - $data = 0; - } elseif (! is_int($data)) { - return false; - } - - return $this->save($key, $data + $offset); - } - - /** - * Performs atomic decrementation of a raw stored value. - * - * @param string $key Cache ID - * @param int $offset Step/value to increase by - * - * @return bool - */ - public function decrement(string $key, int $offset = 1) - { - $key = static::validateKey($key, $this->prefix); - - $data = $this->cache[$key] ?: null; - - if (empty($data)) { - $data = 0; - } elseif (! is_int($data)) { - return false; - } - - return $this->save($key, $data - $offset); - } - - /** - * Will delete all items in the entire cache. - * - * @return bool - */ - public function clean() - { - $this->cache = []; - $this->expirations = []; - - return true; - } - - /** - * Returns information on the entire cache. - * - * The information returned and the structure of the data - * varies depending on the handler. - * - * @return string[] Keys currently present in the store - */ - public function getCacheInfo() - { - return array_keys($this->cache); - } - - /** - * Returns detailed information about the specific item in the cache. - * - * @param string $key Cache item name. - * - * @return array|null - * Returns null if the item does not exist, otherwise array - * with at least the 'expire' key for absolute epoch expiry (or null). - */ - public function getMetaData(string $key) - { - // Misses return null - if (! array_key_exists($key, $this->expirations)) { - return null; - } - - // Count expired items as a miss - if (is_int($this->expirations[$key]) && $this->expirations[$key] > time()) { - return null; - } - - return [ - 'expire' => $this->expirations[$key], - ]; - } - - /** - * Determines if the driver is supported on this system. - * - * @return bool - */ - public function isSupported(): bool - { - return true; - } - - - //-------------------------------------------------------------------- - // Test Helpers - //-------------------------------------------------------------------- - - /** - * Instructs the class to ignore all - * requests to cache an item, and always "miss" - * when checked for existing data. - * - * @return $this - */ - public function bypass(bool $bypass = true) - { - $this->clean(); - - $this->bypass = $bypass; - - return $this; - } - - //-------------------------------------------------------------------- - // Additional Assertions - //-------------------------------------------------------------------- - - /** - * Asserts that the cache has an item named $key. - * The value is not checked since storing false or null - * values is valid. - * - * @param string $key - */ - public function assertHas(string $key) - { - Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`"); - } - - /** - * Asserts that the cache has an item named $key with a value matching $value. - * - * @param string $key - * @param null $value - */ - public function assertHasValue(string $key, $value = null) - { - $item = $this->get($key); - - // Let assertHas handle throwing the error for consistency - // if the key is not found - if (empty($item)) - { - $this->assertHas($key); - } - - Assert::assertEquals($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); - } - - /** - * Asserts that the cache does NOT have an item named $key. - * - * @param string $key - */ - public function assertMissing(string $key) - { - Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists."); - } + /** + * Mock cache storage. + * + * @var array + */ + protected $cache = []; + + /** + * Expiration times. + * + * @var ?int[] + */ + protected $expirations = []; + + /** + * If true, will not cache any data. + * + * @var boolean + */ + protected $bypass = false; + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + } + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $key = static::validateKey($key, $this->prefix); + + return $this->cache[$key] ?? null; + } + + /** + * Get an item from the cache, or execute the given Closure and store the result. + * + * @param string $key Cache item name + * @param integer $ttl Time to live + * @param Closure $callback Callback return value + * + * @return mixed + */ + public function remember(string $key, int $ttl, Closure $callback) + { + $value = $this->get($key); + + if (! is_null($value)) + { + return $value; + } + + $this->save($key, $value = $callback(), $ttl); + + return $value; + } + + /** + * Saves an item to the cache store. + * + * The $raw parameter is only utilized by Mamcache in order to + * allow usage of increment() and decrement(). + * + * @param string $key Cache item name + * @param mixed $value the data to save + * @param integer $ttl Time To Live, in seconds (default 60) + * @param boolean $raw Whether to store the raw value. + * + * @return boolean + */ + public function save(string $key, $value, int $ttl = 60, bool $raw = false) + { + if ($this->bypass) + { + return true; + } + + $key = static::validateKey($key, $this->prefix); + + $this->cache[$key] = $value; + $this->expirations[$key] = $ttl > 0 ? time() + $ttl : null; + + return true; + } + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return boolean + */ + public function delete(string $key) + { + $key = static::validateKey($key, $this->prefix); + + if (! isset($this->cache[$key])) + { + return false; + } + + unset($this->cache[$key], $this->expirations[$key]); + + return true; + } + + /** + * Deletes items from the cache store matching a given pattern. + * + * @param string $pattern Cache items glob-style pattern + * + * @return integer + */ + public function deleteMatching(string $pattern) + { + $count = 0; + + foreach (array_keys($this->cache) as $key) + { + if (fnmatch($pattern, $key)) + { + $count++; + unset($this->cache[$key], $this->expirations[$key]); + } + } + + return $count; + } + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param integer $offset Step/value to increase by + * + * @return boolean + */ + public function increment(string $key, int $offset = 1) + { + $key = static::validateKey($key, $this->prefix); + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data + $offset); + } + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param integer $offset Step/value to increase by + * + * @return boolean + */ + public function decrement(string $key, int $offset = 1) + { + $key = static::validateKey($key, $this->prefix); + + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data - $offset); + } + + /** + * Will delete all items in the entire cache. + * + * @return boolean + */ + public function clean() + { + $this->cache = []; + $this->expirations = []; + + return true; + } + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return string[] Keys currently present in the store + */ + public function getCacheInfo() + { + return array_keys($this->cache); + } + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return array|null + * Returns null if the item does not exist, otherwise array + * with at least the 'expire' key for absolute epoch expiry (or null). + */ + public function getMetaData(string $key) + { + // Misses return null + if (! array_key_exists($key, $this->expirations)) + { + return null; + } + + // Count expired items as a miss + if (is_int($this->expirations[$key]) && $this->expirations[$key] > time()) + { + return null; + } + + return [ + 'expire' => $this->expirations[$key], + ]; + } + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return true; + } + + //-------------------------------------------------------------------- + // Test Helpers + //-------------------------------------------------------------------- + + /** + * Instructs the class to ignore all + * requests to cache an item, and always "miss" + * when checked for existing data. + * + * @return $this + */ + public function bypass(bool $bypass = true) + { + $this->clean(); + + $this->bypass = $bypass; + + return $this; + } + + //-------------------------------------------------------------------- + // Additional Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the cache has an item named $key. + * The value is not checked since storing false or null + * values is valid. + * + * @param string $key + */ + public function assertHas(string $key) + { + Assert::assertNotNull($this->get($key), "The cache does not have an item named: `{$key}`"); + } + + /** + * Asserts that the cache has an item named $key with a value matching $value. + * + * @param string $key + * @param null $value + */ + public function assertHasValue(string $key, $value = null) + { + $item = $this->get($key); + + // Let assertHas handle throwing the error for consistency + // if the key is not found + if (empty($item)) + { + $this->assertHas($key); + } + + Assert::assertEquals($value, $this->get($key), "The cached item `{$key}` does not equal match expectation. Found: " . print_r($value, true)); + } + + /** + * Asserts that the cache does NOT have an item named $key. + * + * @param string $key + */ + public function assertMissing(string $key) + { + Assert::assertArrayNotHasKey($key, $this->cache, "The cached item named `{$key}` exists."); + } }