diff --git a/Cache.php b/Cache.php index fabf32a3a..ff115e715 100644 --- a/Cache.php +++ b/Cache.php @@ -17,7 +17,11 @@ namespace Cake\Cache; use Cake\Cache\Engine\NullEngine; +use Cake\Cache\Exception\CacheWriteException; +use Cake\Cache\Exception\InvalidArgumentException; use Cake\Core\StaticConfigTrait; +use Closure; +use Psr\SimpleCache\CacheInterface; use RuntimeException; /** @@ -55,8 +59,6 @@ * - `MemcacheEngine` - Uses the PECL::Memcache extension and Memcached for storage. * Fast reads/writes, and benefits from memcache being distributed. * - `RedisEngine` - Uses redis and php-redis extension to store cache data. - * - `WincacheEngine` - Uses Windows Cache Extension for PHP. Supports wincache 1.1.0 and higher. - * This engine is recommended to people deploying on windows with IIS. * - `XcacheEngine` - Uses the Xcache extension, an alternative to APCu. * * See Cache engine documentation for expected configuration keys. @@ -71,39 +73,38 @@ class Cache * An array mapping URL schemes to fully qualified caching engine * class names. * - * @var string[] - * @psalm-var array + * @var array + * @phpstan-var array */ - protected static $_dsnClassMap = [ + protected static array $_dsnClassMap = [ 'array' => Engine\ArrayEngine::class, 'apcu' => Engine\ApcuEngine::class, 'file' => Engine\FileEngine::class, 'memcached' => Engine\MemcachedEngine::class, 'null' => Engine\NullEngine::class, 'redis' => Engine\RedisEngine::class, - 'wincache' => Engine\WincacheEngine::class, ]; /** - * Flag for tracking whether or not caching is enabled. + * Flag for tracking whether caching is enabled. * * @var bool */ - protected static $_enabled = true; + protected static bool $_enabled = true; /** * Group to Config mapping * - * @var array + * @var array */ - protected static $_groups = []; + protected static array $_groups = []; /** * Cache Registry used for creating and using cache adapters. * - * @var \Cake\Cache\CacheRegistry|null + * @var \Cake\Cache\CacheRegistry */ - protected static $_registry; + protected static CacheRegistry $_registry; /** * Returns the Cache Registry instance used for creating and using cache adapters. @@ -112,11 +113,7 @@ class Cache */ public static function getRegistry(): CacheRegistry { - if (static::$_registry === null) { - static::$_registry = new CacheRegistry(); - } - - return static::$_registry; + return static::$_registry ??= new CacheRegistry(); } /** @@ -136,7 +133,7 @@ public static function setRegistry(CacheRegistry $registry): void * Finds and builds the instance of the required engine class. * * @param string $name Name of the config array that needs an engine instance built - * @throws \Cake\Cache\InvalidArgumentException When a cache engine cannot be created. + * @throws \Cake\Cache\Exception\InvalidArgumentException When a cache engine cannot be created. * @throws \RuntimeException If loading of the engine failed. * @return void */ @@ -146,11 +143,10 @@ protected static function _buildEngine(string $name): void if (empty(static::$_config[$name]['className'])) { throw new InvalidArgumentException( - sprintf('The "%s" cache configuration does not exist.', $name) + sprintf('The `%s` cache configuration does not exist.', $name), ); } - /** @var array $config */ $config = static::$_config[$name]; try { @@ -169,13 +165,14 @@ protected static function _buildEngine(string $name): void if ($config['fallback'] === $name) { throw new InvalidArgumentException(sprintf( - '"%s" cache configuration cannot fallback to itself.', - $name + '`%s` cache configuration cannot fallback to itself.', + $name, ), 0, $e); } - /** @var \Cake\Cache\CacheEngine $fallbackEngine */ $fallbackEngine = clone static::pool($config['fallback']); + assert($fallbackEngine instanceof CacheEngine); + $newConfig = $config + ['groups' => [], 'prefix' => null]; $fallbackEngine->setConfig('groups', $newConfig['groups'], false); if ($newConfig['prefix']) { @@ -189,6 +186,7 @@ protected static function _buildEngine(string $name): void } if (!empty($config['groups'])) { + /** @var string $group */ foreach ($config['groups'] as $group) { static::$_groups[$group][] = $name; static::$_groups[$group] = array_unique(static::$_groups[$group]); @@ -197,27 +195,13 @@ protected static function _buildEngine(string $name): void } } - /** - * Get a cache engine object for the named cache config. - * - * @param string $config The name of the configured cache backend. - * @return \Psr\SimpleCache\CacheInterface&\Cake\Cache\CacheEngineInterface - * @deprecated 3.7.0 Use {@link pool()} instead. This method will be removed in 5.0. - */ - public static function engine(string $config) - { - deprecationWarning('Cache::engine() is deprecated. Use Cache::pool() instead.'); - - return static::pool($config); - } - /** * Get a SimpleCacheEngine object for the named cache pool. * * @param string $config The name of the configured cache backend. * @return \Psr\SimpleCache\CacheInterface&\Cake\Cache\CacheEngineInterface */ - public static function pool(string $config) + public static function pool(string $config): CacheInterface&CacheEngineInterface { if (!static::$_enabled) { return new NullEngine(); @@ -225,13 +209,13 @@ public static function pool(string $config) $registry = static::getRegistry(); - if (isset($registry->{$config})) { - return $registry->{$config}; + if ($registry->has($config)) { + return $registry->get($config); } static::_buildEngine($config); - return $registry->{$config}; + return $registry->get($config); } /** @@ -256,7 +240,7 @@ public static function pool(string $config) * @param string $config Optional string configuration name to write to. Defaults to 'default' * @return bool True if the data was successfully cached, false on failure */ - public static function write(string $key, $value, string $config = 'default'): bool + public static function write(string $key, mixed $value, string $config = 'default'): bool { if (is_resource($value)) { return false; @@ -265,15 +249,12 @@ public static function write(string $key, $value, string $config = 'default'): b $backend = static::pool($config); $success = $backend->set($key, $value); if ($success === false && $value !== '') { - trigger_error( - sprintf( - "%s cache was unable to write '%s' to %s cache", - $config, - $key, - get_class($backend) - ), - E_USER_WARNING - ); + throw new CacheWriteException(sprintf( + "%s cache was unable to write '%s' to %s cache", + $config, + $key, + $backend::class, + )); } return $success; @@ -299,7 +280,7 @@ public static function write(string $key, $value, string $config = 'default'): b * @param iterable $data An array or Traversable of data to be stored in the cache * @param string $config Optional string configuration name to write to. Defaults to 'default' * @return bool True on success, false on failure - * @throws \Cake\Cache\InvalidArgumentException + * @throws \Cake\Cache\Exception\InvalidArgumentException */ public static function writeMany(iterable $data, string $config = 'default'): bool { @@ -328,7 +309,7 @@ public static function writeMany(iterable $data, string $config = 'default'): bo * @return mixed The cached data, or null if the data doesn't exist, has expired, * or if there was an error fetching it. */ - public static function read(string $key, string $config = 'default') + public static function read(string $key, string $config = 'default'): mixed { return static::pool($config)->get($key); } @@ -350,11 +331,11 @@ public static function read(string $key, string $config = 'default') * Cache::readMany(['my_data_1', 'my_data_2], 'long_term'); * ``` * - * @param iterable $keys An array or Traversable of keys to fetch from the cache + * @param iterable $keys An array or Traversable of keys to fetch from the cache * @param string $config optional name of the configuration to use. Defaults to 'default' * @return iterable An array containing, for each of the given $keys, * the cached data or false if cached data could not be retrieved. - * @throws \Cake\Cache\InvalidArgumentException + * @throws \Cake\Cache\Exception\InvalidArgumentException */ public static function readMany(iterable $keys, string $config = 'default'): iterable { @@ -369,12 +350,12 @@ public static function readMany(iterable $keys, string $config = 'default'): ite * @param string $config Optional string configuration name. Defaults to 'default' * @return int|false New value, or false if the data doesn't exist, is not integer, * or if there was an error fetching it. - * @throws \Cake\Cache\InvalidArgumentException When offset < 0 + * @throws \Cake\Cache\Exception\InvalidArgumentException When offset < 0 */ - public static function increment(string $key, int $offset = 1, string $config = 'default') + public static function increment(string $key, int $offset = 1, string $config = 'default'): int|false { if ($offset < 0) { - throw new InvalidArgumentException('Offset cannot be less than 0.'); + throw new InvalidArgumentException('Offset cannot be less than `0`.'); } return static::pool($config)->increment($key, $offset); @@ -388,12 +369,12 @@ public static function increment(string $key, int $offset = 1, string $config = * @param string $config Optional string configuration name. Defaults to 'default' * @return int|false New value, or false if the data doesn't exist, is not integer, * or if there was an error fetching it - * @throws \Cake\Cache\InvalidArgumentException when offset < 0 + * @throws \Cake\Cache\Exception\InvalidArgumentException when offset < 0 */ - public static function decrement(string $key, int $offset = 1, string $config = 'default') + public static function decrement(string $key, int $offset = 1, string $config = 'default'): int|false { if ($offset < 0) { - throw new InvalidArgumentException('Offset cannot be less than 0.'); + throw new InvalidArgumentException('Offset cannot be less than `0`.'); } return static::pool($config)->decrement($key, $offset); @@ -442,10 +423,10 @@ public static function delete(string $key, string $config = 'default'): bool * Cache::deleteMany(['my_data_1', 'my_data_2], 'long_term'); * ``` * - * @param iterable $keys Array or Traversable of cache keys to be deleted + * @param iterable $keys Array or Traversable of cache keys to be deleted * @param string $config name of the configuration to use. Defaults to 'default' * @return bool True on success, false on failure. - * @throws \Cake\Cache\InvalidArgumentException + * @throws \Cake\Cache\Exception\InvalidArgumentException */ public static function deleteMany(iterable $keys, string $config = 'default'): bool { @@ -466,7 +447,7 @@ public static function clear(string $config = 'default'): bool /** * Delete all keys from the cache from all configurations. * - * @return bool[] Status code. For each configuration, it reports the status of the operation + * @return array Status code. For each configuration, it reports the status of the operation */ public static function clearAll(): array { @@ -503,9 +484,9 @@ public static function clearGroup(string $group, string $config = 'default'): bo * $configs will equal to `['posts' => ['daily', 'weekly']]` * Calling this method will load all the configured engines. * - * @param string|null $group group name or null to retrieve all group mappings - * @return array map of group and all configuration that has the same group - * @throws \Cake\Cache\InvalidArgumentException + * @param string|null $group Group name or null to retrieve all group mappings + * @return array Map of group and all configuration that has the same group + * @throws \Cake\Cache\Exception\InvalidArgumentException */ public static function groupConfigs(?string $group = null): array { @@ -520,7 +501,7 @@ public static function groupConfigs(?string $group = null): array return [$group => self::$_groups[$group]]; } - throw new InvalidArgumentException(sprintf('Invalid cache group %s', $group)); + throw new InvalidArgumentException(sprintf('Invalid cache group `%s`.', $group)); } /** @@ -548,7 +529,7 @@ public static function disable(): void } /** - * Check whether or not caching is enabled. + * Check whether caching is enabled. * * @return bool */ @@ -560,8 +541,8 @@ public static function enabled(): bool /** * Provides the ability to easily do read-through caching. * - * When called if the $key is not set in $config, the $callable function - * will be invoked. The results will then be stored into the cache config + * If the key is not set, the default callback is run to get the default value. + * The results will then be stored into the cache config * at key. * * Examples: @@ -575,21 +556,20 @@ public static function enabled(): bool * ``` * * @param string $key The cache key to read/store data at. - * @param callable $callable The callable that provides data in the case when - * the cache key is empty. Can be any callable type supported by your PHP. + * @param \Closure $default The callback that provides data in the case when + * the cache key is empty. * @param string $config The cache configuration to use for this operation. * Defaults to default. - * @return mixed If the key is found: the cached data, false if the data - * missing/expired, or an error. If the key is not found: boolean of the - * success of the write + * @return mixed If the key is found: the cached data. + * If the key is not found the value returned by the the default callback. */ - public static function remember(string $key, callable $callable, string $config = 'default') + public static function remember(string $key, Closure $default, string $config = 'default'): mixed { $existing = self::read($key, $config); if ($existing !== null) { return $existing; } - $results = $callable(); + $results = $default(); self::write($key, $results, $config); return $results; @@ -618,7 +598,7 @@ public static function remember(string $key, callable $callable, string $config * @return bool True if the data was successfully cached, false on failure. * Or if the key existed already. */ - public static function add(string $key, $value, string $config = 'default'): bool + public static function add(string $key, mixed $value, string $config = 'default'): bool { if (is_resource($value)) { return false; diff --git a/CacheEngine.php b/CacheEngine.php index 56db69536..da15733c7 100644 --- a/CacheEngine.php +++ b/CacheEngine.php @@ -16,15 +16,29 @@ */ namespace Cake\Cache; +use Cake\Cache\Event\CacheAfterAddEvent; +use Cake\Cache\Event\CacheBeforeAddEvent; +use Cake\Cache\Exception\InvalidArgumentException; use Cake\Core\InstanceConfigTrait; +use Cake\Event\EventDispatcherInterface; +use Cake\Event\EventDispatcherTrait; use DateInterval; +use DateTime; use Psr\SimpleCache\CacheInterface; +use function Cake\Core\triggerWarning; /** * Storage engine for CakePHP caching + * + * @template TSubject of \Cake\Cache\CacheEngine + * @implements \Cake\Event\EventDispatcherInterface */ -abstract class CacheEngine implements CacheInterface, CacheEngineInterface +abstract class CacheEngine implements CacheInterface, CacheEngineInterface, EventDispatcherInterface { + /** + * @use \Cake\Event\EventDispatcherTrait + */ + use EventDispatcherTrait; use InstanceConfigTrait; /** @@ -49,9 +63,9 @@ abstract class CacheEngine implements CacheInterface, CacheEngineInterface * - `warnOnWriteFailures` Some engines, such as ApcuEngine, may raise warnings on * write failures. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'duration' => 3600, 'groups' => [], 'prefix' => 'cake_', @@ -64,7 +78,7 @@ abstract class CacheEngine implements CacheInterface, CacheEngineInterface * * @var string */ - protected $_groupPrefix = ''; + protected string $_groupPrefix = ''; /** * Initialize the cache engine @@ -72,7 +86,7 @@ abstract class CacheEngine implements CacheInterface, CacheEngineInterface * Called automatically by the cache frontend. Merge the runtime config with the defaults * before use. * - * @param array $config Associative array of parameters for the engine + * @param array $config Associative array of parameters for the engine * @return bool True if the engine has been successfully initialized, false if not */ public function init(array $config = []): bool @@ -93,13 +107,13 @@ public function init(array $config = []): bool /** * Ensure the validity of the given cache key. * - * @param string $key Key to check. + * @param mixed $key Key to check. * @return void - * @throws \Cake\Cache\InvalidArgumentException When the key is not valid. + * @throws \Cake\Cache\Exception\InvalidArgumentException When the key is not valid. */ - protected function ensureValidKey($key): void + protected function ensureValidKey(mixed $key): void { - if (!is_string($key) || strlen($key) === 0) { + if (!is_string($key) || $key === '') { throw new InvalidArgumentException('A cache key must be a non-empty string.'); } } @@ -110,17 +124,10 @@ protected function ensureValidKey($key): void * @param iterable $iterable The iterable to check. * @param string $check Whether to check keys or values. * @return void - * @throws \Cake\Cache\InvalidArgumentException + * @throws \Cake\Cache\Exception\InvalidArgumentException */ - protected function ensureValidType($iterable, string $check = self::CHECK_VALUE): void + protected function ensureValidType(iterable $iterable, string $check = self::CHECK_VALUE): void { - if (!is_iterable($iterable)) { - throw new InvalidArgumentException(sprintf( - 'A cache %s must be either an array or a Traversable.', - $check === self::CHECK_VALUE ? 'key set' : 'set' - )); - } - foreach ($iterable as $key => $value) { if ($check === self::CHECK_VALUE) { $this->ensureValidKey($value); @@ -133,13 +140,13 @@ protected function ensureValidType($iterable, string $check = self::CHECK_VALUE) /** * Obtains multiple cache items by their unique keys. * - * @param iterable $keys A list of keys that can obtained in a single operation. + * @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, + * @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\Exception\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): iterable + public function getMultiple(iterable $keys, mixed $default = null): iterable { $this->ensureValidType($keys); @@ -159,13 +166,14 @@ public function getMultiple($keys, $default = null): iterable * 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, + * @throws \Cake\Cache\Exception\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): bool + public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool { $this->ensureValidType($values, self::CHECK_KEY); + $restore = null; if ($ttl !== null) { $restore = $this->getConfig('duration'); $this->setConfig('duration', $ttl); @@ -180,32 +188,36 @@ public function setMultiple($values, $ttl = null): bool return true; } finally { - if (isset($restore)) { + if ($restore !== null) { $this->setConfig('duration', $restore); } } } /** - * Deletes multiple cache items in a single operation. + * Deletes multiple cache items as a list + * + * This is a best effort attempt. If deleting an item would + * create an error it will be ignored, and all items will + * be attempted. * * @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, + * @throws \Cake\Cache\Exception\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): bool + public function deleteMultiple(iterable $keys): bool { $this->ensureValidType($keys); + $result = true; foreach ($keys as $key) { - $result = $this->delete($key); - if ($result === false) { - return false; + if (!$this->delete($key)) { + $result = false; } } - return true; + return $result; } /** @@ -218,9 +230,9 @@ public function deleteMultiple($keys): bool * * @param string $key The cache item key. * @return bool - * @throws \Cake\Cache\InvalidArgumentException If the $key string is not a legal value. + * @throws \Cake\Cache\Exception\InvalidArgumentException If the $key string is not a legal value. */ - public function has($key): bool + public function has(string $key): bool { return $this->get($key) !== null; } @@ -231,9 +243,9 @@ public function has($key): bool * @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. + * @throws \Cake\Cache\Exception\InvalidArgumentException If the $key string is not a legal value. */ - abstract public function get($key, $default = null); + abstract public function get(string $key, mixed $default = null): mixed; /** * Persists data in the cache, uniquely referenced by the given key with an optional expiration TTL time. @@ -244,10 +256,10 @@ abstract public function get($key, $default = null); * 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 + * @throws \Cake\Cache\Exception\InvalidArgumentException * MUST be thrown if the $key string is not a legal value. */ - abstract public function set($key, $value, $ttl = null): bool; + abstract public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool; /** * Increment a number under the key and return incremented value @@ -256,16 +268,16 @@ abstract public function set($key, $value, $ttl = null): bool; * @param int $offset How much to add * @return int|false New incremented value, false otherwise */ - abstract public function increment(string $key, int $offset = 1); + abstract public function increment(string $key, int $offset = 1): int|false; /** * Decrement a number under the key and return decremented value * * @param string $key Identifier for the data * @param int $offset How much to subtract - * @return int|false New incremented value, false otherwise + * @return int|false New decremented value, false otherwise */ - abstract public function decrement(string $key, int $offset = 1); + abstract public function decrement(string $key, int $offset = 1): int|false; /** * Delete a key from the cache @@ -273,7 +285,7 @@ abstract public function decrement(string $key, int $offset = 1); * @param string $key Identifier for the data * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed */ - abstract public function delete($key): bool; + abstract public function delete(string $key): bool; /** * Delete all keys from the cache @@ -292,12 +304,32 @@ abstract public function clear(): bool; * @param mixed $value Data to be cached. * @return bool True if the data was successfully cached, false on failure. */ - public function add(string $key, $value): bool + public function add(string $key, mixed $value): bool { $cachedValue = $this->get($key); + $prefixedKey = $this->_key($key); + $duration = $this->getConfig('duration'); + + $this->_eventClass = CacheBeforeAddEvent::class; + $this->dispatchEvent(CacheBeforeAddEvent::NAME, [ + 'key' => $prefixedKey, + 'value' => $value, + 'ttl' => $duration, + ]); + if ($cachedValue === null) { - return $this->set($key, $value); + $success = $this->set($key, $value); + $this->_eventClass = CacheAfterAddEvent::class; + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $prefixedKey, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); + + return $success; } + $this->_eventClass = CacheAfterAddEvent::class; + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $prefixedKey, 'value' => $value, 'success' => false, 'ttl' => $duration, + ]); return false; } @@ -317,7 +349,7 @@ abstract public function clearGroup(string $group): bool; * and returns the `group value` for each of them, this is * the token representing each group in the cache key * - * @return string[] + * @return array */ public function groups(): array { @@ -332,15 +364,15 @@ public function groups(): array * * @param string $key the key passed over * @return string Prefixed key with potentially unsafe characters replaced. - * @throws \Cake\Cache\InvalidArgumentException If key's value is invalid. + * @throws \Cake\Cache\Exception\InvalidArgumentException If key's value is invalid. */ - protected function _key($key): string + protected function _key(string $key): string { $this->ensureValidKey($key); $prefix = ''; if ($this->_groupPrefix) { - $prefix = md5(implode('_', $this->groups())); + $prefix = hash('xxh128', implode('_', $this->groups())); } $key = preg_replace('/[\s]+/', '_', $key); @@ -370,7 +402,7 @@ protected function warning(string $message): void * driver's default duration will be used. * @return int */ - protected function duration($ttl): int + protected function duration(DateInterval|int|null $ttl): int { if ($ttl === null) { return $this->_config['duration']; @@ -378,10 +410,12 @@ protected function duration($ttl): int if (is_int($ttl)) { return $ttl; } - if ($ttl instanceof DateInterval) { - return (int)$ttl->format('%s'); - } - throw new InvalidArgumentException('TTL values must be one of null, int, \DateInterval'); + /** @var \DateTime $datetime */ + $datetime = DateTime::createFromFormat('U', '0'); + + return (int)$datetime + ->add($ttl) + ->format('U'); } } diff --git a/CacheEngineInterface.php b/CacheEngineInterface.php index d7eefe39f..9f6f2ce52 100644 --- a/CacheEngineInterface.php +++ b/CacheEngineInterface.php @@ -35,7 +35,7 @@ interface CacheEngineInterface * @return bool True if the data was successfully cached, false on failure. * Or if the key existed already. */ - public function add(string $key, $value): bool; + public function add(string $key, mixed $value): bool; /** * Increment a number under the key and return incremented value @@ -44,16 +44,16 @@ public function add(string $key, $value): bool; * @param int $offset How much to add * @return int|false New incremented value, false otherwise */ - public function increment(string $key, int $offset = 1); + public function increment(string $key, int $offset = 1): int|false; /** * Decrement a number under the key and return decremented value * * @param string $key Identifier for the data * @param int $offset How much to subtract - * @return int|false New incremented value, false otherwise + * @return int|false New decremented value, false otherwise */ - public function decrement(string $key, int $offset = 1); + public function decrement(string $key, int $offset = 1): int|false; /** * Clear all values belonging to the named group. diff --git a/CacheRegistry.php b/CacheRegistry.php index fb6d0ab06..5f7f78492 100644 --- a/CacheRegistry.php +++ b/CacheRegistry.php @@ -18,13 +18,13 @@ use BadMethodCallException; use Cake\Core\App; +use Cake\Core\Exception\CakeException; use Cake\Core\ObjectRegistry; -use RuntimeException; /** * An object registry for cache engines. * - * Used by Cake\Cache\Cache to load and manage cache engines. + * Used by {@link \Cake\Cache\Cache} to load and manage cache engines. * * @extends \Cake\Core\ObjectRegistry<\Cake\Cache\CacheEngine> */ @@ -36,11 +36,11 @@ class CacheRegistry extends ObjectRegistry * Part of the template method for Cake\Core\ObjectRegistry::load() * * @param string $class Partial classname to resolve. - * @return string|null Either the correct classname or null. - * @psalm-return class-string|null + * @return class-string<\Cake\Cache\CacheEngine>|null Either the correct classname or null. */ protected function _resolveClassName(string $class): ?string { + /** @var class-string<\Cake\Cache\CacheEngine>|null */ return App::className($class, 'Cache/Engine', 'Engine'); } @@ -56,7 +56,7 @@ protected function _resolveClassName(string $class): ?string */ protected function _throwMissingClassError(string $class, ?string $plugin): void { - throw new BadMethodCallException(sprintf('Cache engine %s is not available.', $class)); + throw new BadMethodCallException(sprintf('Cache engine `%s` is not available.', $class)); } /** @@ -64,13 +64,13 @@ protected function _throwMissingClassError(string $class, ?string $plugin): void * * Part of the template method for Cake\Core\ObjectRegistry::load() * - * @param string|\Cake\Cache\CacheEngine $class The classname or object to make. + * @param \Cake\Cache\CacheEngine|class-string<\Cake\Cache\CacheEngine> $class The classname or object to make. * @param string $alias The alias of the object. - * @param array $config An array of settings to use for the cache engine. + * @param array $config An array of settings to use for the cache engine. * @return \Cake\Cache\CacheEngine The constructed CacheEngine class. - * @throws \RuntimeException when an object doesn't implement the correct interface. + * @throws \Cake\Core\Exception\CakeException When the cache engine cannot be initialized. */ - protected function _create($class, string $alias, array $config): CacheEngine + protected function _create(object|string $class, string $alias, array $config): CacheEngine { if (is_object($class)) { $instance = $class; @@ -79,18 +79,14 @@ protected function _create($class, string $alias, array $config): CacheEngine } unset($config['className']); - if (!($instance instanceof CacheEngine)) { - throw new RuntimeException( - 'Cache engines must use Cake\Cache\CacheEngine as a base class.' - ); - } + assert($instance instanceof CacheEngine, 'Cache engines must extend `' . CacheEngine::class . '`.'); if (!$instance->init($config)) { - throw new RuntimeException( + throw new CakeException( sprintf( - 'Cache engine %s is not properly configured. Check error log for additional information.', - get_class($instance) - ) + 'Cache engine `%s` is not properly configured. Check error log for additional information.', + $instance::class, + ), ); } diff --git a/Engine/ApcuEngine.php b/Engine/ApcuEngine.php index 170ea4a43..506b0620e 100644 --- a/Engine/ApcuEngine.php +++ b/Engine/ApcuEngine.php @@ -18,7 +18,22 @@ use APCUIterator; use Cake\Cache\CacheEngine; -use RuntimeException; +use Cake\Cache\Event\CacheAfterAddEvent; +use Cake\Cache\Event\CacheAfterDecrementEvent; +use Cake\Cache\Event\CacheAfterDeleteEvent; +use Cake\Cache\Event\CacheAfterGetEvent; +use Cake\Cache\Event\CacheAfterIncrementEvent; +use Cake\Cache\Event\CacheAfterSetEvent; +use Cake\Cache\Event\CacheBeforeAddEvent; +use Cake\Cache\Event\CacheBeforeDecrementEvent; +use Cake\Cache\Event\CacheBeforeDeleteEvent; +use Cake\Cache\Event\CacheBeforeGetEvent; +use Cake\Cache\Event\CacheBeforeIncrementEvent; +use Cake\Cache\Event\CacheBeforeSetEvent; +use Cake\Cache\Event\CacheClearedEvent; +use Cake\Cache\Event\CacheGroupClearEvent; +use Cake\Core\Exception\CakeException; +use DateInterval; /** * APCu storage engine for cache @@ -29,22 +44,22 @@ class ApcuEngine extends CacheEngine * Contains the compiled group names * (prefixed with the global configuration prefix) * - * @var string[] + * @var array */ - protected $_compiledGroupNames = []; + protected array $_compiledGroupNames = []; /** * Initialize the Cache Engine * * Called automatically by the cache frontend * - * @param array $config array of setting for the engine + * @param array $config array of setting for the engine * @return bool True if the engine has been successfully initialized, false if not */ public function init(array $config = []): bool { if (!extension_loaded('apcu')) { - throw new RuntimeException('The `apcu` extension must be enabled to use ApcuEngine.'); + throw new CakeException('The `apcu` extension must be enabled to use ApcuEngine.'); } return parent::init($config); @@ -61,12 +76,22 @@ public function init(array $config = []): bool * @return bool True on success and false on failure. * @link https://secure.php.net/manual/en/function.apcu-store.php */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { $key = $this->_key($key); $duration = $this->duration($ttl); - return apcu_store($key, $value, $duration); + $this->_eventClass = CacheBeforeSetEvent::class; + $this->dispatchEvent(CacheBeforeSetEvent::NAME, ['key' => $key, 'value' => $value, 'ttl' => $duration]); + + $success = apcu_store($key, $value, $duration); + + $this->_eventClass = CacheAfterSetEvent::class; + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); + + return $success; } /** @@ -78,9 +103,16 @@ public function set($key, $value, $ttl = null): bool * has expired, or if there was an error fetching it * @link https://secure.php.net/manual/en/function.apcu-fetch.php */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { - $value = apcu_fetch($this->_key($key), $success); + $key = $this->_key($key); + $this->_eventClass = CacheBeforeGetEvent::class; + $this->dispatchEvent(CacheBeforeGetEvent::NAME, ['key' => $key, 'default' => $default]); + + $value = apcu_fetch($key, $success); + + $this->_eventClass = CacheAfterGetEvent::class; + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $value, 'success' => $success]); if ($success === false) { return $default; } @@ -96,11 +128,20 @@ public function get($key, $default = null) * @return int|false New incremented value, false otherwise * @link https://secure.php.net/manual/en/function.apcu-inc.php */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { $key = $this->_key($key); + $this->_eventClass = CacheBeforeIncrementEvent::class; + $this->dispatchEvent(CacheBeforeIncrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + + $value = apcu_inc($key, $offset); - return apcu_inc($key, $offset); + $this->_eventClass = CacheAfterIncrementEvent::class; + $this->dispatchEvent(CacheAfterIncrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $value !== false, 'value' => $value, + ]); + + return $value; } /** @@ -111,11 +152,20 @@ public function increment(string $key, int $offset = 1) * @return int|false New decremented value, false otherwise * @link https://secure.php.net/manual/en/function.apcu-dec.php */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { $key = $this->_key($key); + $this->_eventClass = CacheBeforeDecrementEvent::class; + $this->dispatchEvent(CacheBeforeDecrementEvent::NAME, ['key' => $key, 'offset' => $offset]); - return apcu_dec($key, $offset); + $result = apcu_dec($key, $offset); + + $this->_eventClass = CacheAfterDecrementEvent::class; + $this->dispatchEvent(CacheAfterDecrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $result !== false, 'value' => $result, + ]); + + return $result; } /** @@ -125,11 +175,18 @@ public function decrement(string $key, int $offset = 1) * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed * @link https://secure.php.net/manual/en/function.apcu-delete.php */ - public function delete($key): bool + public function delete(string $key): bool { $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + + $result = apcu_delete($key); + + $this->_eventClass = CacheAfterDeleteEvent::class; + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => $result]); - return apcu_delete($key); + return $result; } /** @@ -144,20 +201,25 @@ public function clear(): bool if (class_exists(APCUIterator::class, false)) { $iterator = new APCUIterator( '/^' . preg_quote($this->_config['prefix'], '/') . '/', - APC_ITER_NONE + APC_ITER_NONE, ); apcu_delete($iterator); + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); return true; } $cache = apcu_cache_info(); // Raises warning by itself already foreach ($cache['cache_list'] as $key) { - if (strpos($key['info'], $this->_config['prefix']) === 0) { + if (str_starts_with($key['info'], $this->_config['prefix'])) { apcu_delete($key['info']); } } + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); + return true; } @@ -170,12 +232,23 @@ public function clear(): bool * @return bool True if the data was successfully cached, false on failure. * @link https://secure.php.net/manual/en/function.apcu-add.php */ - public function add(string $key, $value): bool + public function add(string $key, mixed $value): bool { $key = $this->_key($key); $duration = $this->_config['duration']; + $this->_eventClass = CacheBeforeAddEvent::class; + $this->dispatchEvent(CacheBeforeAddEvent::NAME, [ + 'key' => $key, 'value' => $value, 'ttl' => $duration, + ]); + + $result = apcu_add($key, $value, $duration); - return apcu_add($key, $value, $duration); + $this->_eventClass = CacheAfterAddEvent::class; + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $result, 'ttl' => $duration, + ]); + + return $result; } /** @@ -183,13 +256,13 @@ public function add(string $key, $value): bool * If the group initial value was not found, then it initializes * the group accordingly. * - * @return string[] + * @return array * @link https://secure.php.net/manual/en/function.apcu-fetch.php * @link https://secure.php.net/manual/en/function.apcu-store.php */ public function groups(): array { - if (empty($this->_compiledGroupNames)) { + if (!$this->_compiledGroupNames) { foreach ($this->_config['groups'] as $group) { $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; } @@ -203,7 +276,7 @@ public function groups(): array $value = 1; if (apcu_store($group, $value) === false) { $this->warning( - sprintf('Failed to store key "%s" with value "%s" into APCu cache.', $group, $value) + sprintf('Failed to store key `%s` with value `%s` into APCu cache.', $group, $value), ); } $groups[$group] = $value; @@ -233,6 +306,8 @@ public function clearGroup(string $group): bool { $success = false; apcu_inc($this->_config['prefix'] . $group, 1, $success); + $this->_eventClass = CacheGroupClearEvent::class; + $this->dispatchEvent(CacheGroupClearEvent::NAME, ['group' => $group]); return $success; } diff --git a/Engine/ArrayEngine.php b/Engine/ArrayEngine.php index c33608204..094131bde 100644 --- a/Engine/ArrayEngine.php +++ b/Engine/ArrayEngine.php @@ -17,6 +17,19 @@ namespace Cake\Cache\Engine; use Cake\Cache\CacheEngine; +use Cake\Cache\Event\CacheAfterDecrementEvent; +use Cake\Cache\Event\CacheAfterDeleteEvent; +use Cake\Cache\Event\CacheAfterGetEvent; +use Cake\Cache\Event\CacheAfterIncrementEvent; +use Cake\Cache\Event\CacheAfterSetEvent; +use Cake\Cache\Event\CacheBeforeDecrementEvent; +use Cake\Cache\Event\CacheBeforeDeleteEvent; +use Cake\Cache\Event\CacheBeforeGetEvent; +use Cake\Cache\Event\CacheBeforeIncrementEvent; +use Cake\Cache\Event\CacheBeforeSetEvent; +use Cake\Cache\Event\CacheClearedEvent; +use Cake\Cache\Event\CacheGroupClearEvent; +use DateInterval; /** * Array storage engine for cache. @@ -35,9 +48,9 @@ class ArrayEngine extends CacheEngine * * Structured as [key => [exp => expiration, val => value]] * - * @var array + * @var array */ - protected $data = []; + protected array $data = []; /** * Write data for key into cache @@ -49,12 +62,23 @@ class ArrayEngine extends CacheEngine * for it or let the driver take care of that. * @return bool True on success and false on failure. */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { $key = $this->_key($key); $expires = time() + $this->duration($ttl); + + $this->_eventClass = CacheBeforeSetEvent::class; + $this->dispatchEvent(CacheBeforeSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'ttl' => $this->duration($ttl), + ]); + $this->data[$key] = ['exp' => $expires, 'val' => $value]; + $this->_eventClass = CacheAfterSetEvent::class; + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => true, 'ttl' => $this->duration($ttl), + ]); + return true; } @@ -66,10 +90,16 @@ public function set($key, $value, $ttl = null): bool * @return mixed The cached data, or default value if the data doesn't exist, has * expired, or if there was an error fetching it. */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { $key = $this->_key($key); + $this->_eventClass = CacheBeforeGetEvent::class; + $this->dispatchEvent(CacheBeforeGetEvent::NAME, ['key' => $key, 'default' => $default]); + + $this->_eventClass = CacheAfterGetEvent::class; if (!isset($this->data[$key])) { + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); + return $default; } $data = $this->data[$key]; @@ -78,10 +108,13 @@ public function get($key, $default = null) $now = time(); if ($data['exp'] <= $now) { unset($this->data[$key]); + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); return $default; } + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $data['val'], 'success' => true]); + return $data['val']; } @@ -92,15 +125,24 @@ public function get($key, $default = null) * @param int $offset How much to increment * @return int|false New incremented value, false otherwise */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { if ($this->get($key) === null) { $this->set($key, 0); } $key = $this->_key($key); + $this->_eventClass = CacheBeforeIncrementEvent::class; + $this->dispatchEvent(CacheBeforeIncrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + $this->data[$key]['val'] += $offset; + $val = $this->data[$key]['val']; - return $this->data[$key]['val']; + $this->_eventClass = CacheAfterIncrementEvent::class; + $this->dispatchEvent('Cache.afterIncrement', [ + 'key' => $key, 'offset' => $offset, 'success' => true, 'value' => $val, + ]); + + return $val; } /** @@ -110,14 +152,22 @@ public function increment(string $key, int $offset = 1) * @param int $offset How much to subtract * @return int|false New decremented value, false otherwise */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { if ($this->get($key) === null) { $this->set($key, 0); } $key = $this->_key($key); + $this->_eventClass = CacheBeforeDecrementEvent::class; + $this->dispatchEvent(CacheBeforeDecrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + $this->data[$key]['val'] -= $offset; + $this->_eventClass = CacheAfterDecrementEvent::class; + $this->dispatchEvent(CacheAfterDecrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => true, 'value' => $this->data[$key]['val'], + ]); + return $this->data[$key]['val']; } @@ -127,11 +177,17 @@ public function decrement(string $key, int $offset = 1) * @param string $key Identifier for the data * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed */ - public function delete($key): bool + public function delete(string $key): bool { $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + unset($this->data[$key]); + $this->_eventClass = CacheAfterDeleteEvent::class; + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => true]); + return true; } @@ -143,6 +199,8 @@ public function delete($key): bool public function clear(): bool { $this->data = []; + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); return true; } @@ -152,16 +210,14 @@ public function clear(): bool * If the group initial value was not found, then it initializes * the group accordingly. * - * @return string[] + * @return array */ public function groups(): array { $result = []; foreach ($this->_config['groups'] as $group) { $key = $this->_config['prefix'] . $group; - if (!isset($this->data[$key])) { - $this->data[$key] = ['exp' => PHP_INT_MAX, 'val' => 1]; - } + $this->data[$key] ??= ['exp' => PHP_INT_MAX, 'val' => 1]; $value = $this->data[$key]['val']; $result[] = $group . $value; } @@ -182,6 +238,8 @@ public function clearGroup(string $group): bool if (isset($this->data[$key])) { $this->data[$key]['val'] += 1; } + $this->_eventClass = CacheGroupClearEvent::class; + $this->dispatchEvent(CacheGroupClearEvent::NAME, ['group' => $group]); return true; } diff --git a/Engine/FileEngine.php b/Engine/FileEngine.php index 22ec33ce0..1836f9296 100644 --- a/Engine/FileEngine.php +++ b/Engine/FileEngine.php @@ -17,8 +17,16 @@ namespace Cake\Cache\Engine; use Cake\Cache\CacheEngine; -use Cake\Cache\InvalidArgumentException; +use Cake\Cache\Event\CacheAfterDeleteEvent; +use Cake\Cache\Event\CacheAfterGetEvent; +use Cake\Cache\Event\CacheAfterSetEvent; +use Cake\Cache\Event\CacheBeforeDeleteEvent; +use Cake\Cache\Event\CacheBeforeGetEvent; +use Cake\Cache\Event\CacheBeforeSetEvent; +use Cake\Cache\Event\CacheClearedEvent; +use Cake\Cache\Event\CacheGroupClearEvent; use CallbackFilterIterator; +use DateInterval; use Exception; use FilesystemIterator; use LogicException; @@ -39,9 +47,9 @@ class FileEngine extends CacheEngine /** * Instance of SplFileObject class * - * @var \SplFileObject|null + * @var \SplFileObject */ - protected $_File; + protected SplFileObject $_File; /** * The default config used unless overridden by runtime configuration @@ -51,19 +59,20 @@ class FileEngine extends CacheEngine * handy for deleting a complete group from cache. * - `lock` Used by FileCache. Should files be locked before writing to them? * - `mask` The mask used for created files - * - `path` Path to where cachefiles should be saved. Defaults to system's temp dir. + * - `dirMask` The mask used for created folders + * - `path` Path to where cache files should be saved. Defaults to system's temp dir. * - `prefix` Prepended to all entries. Good for when you need to share a keyspace * with either another cache config or another application. - * cache::gc from ever being called automatically. * - `serialize` Should cache objects be serialized first. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'duration' => 3600, 'groups' => [], 'lock' => true, 'mask' => 0664, + 'dirMask' => 0777, 'path' => null, 'prefix' => 'cake_', 'serialize' => true, @@ -74,23 +83,21 @@ class FileEngine extends CacheEngine * * @var bool */ - protected $_init = true; + protected bool $_init = true; /** * Initialize File Cache Engine * * Called automatically by the cache frontend. * - * @param array $config array of setting for the engine + * @param array $config array of setting for the engine * @return bool True if the engine has been successfully initialized, false if not */ public function init(array $config = []): bool { parent::init($config); - if ($this->_config['path'] === null) { - $this->_config['path'] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cake_cache' . DIRECTORY_SEPARATOR; - } + $this->_config['path'] ??= sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'cake_cache' . DIRECTORY_SEPARATOR; if (substr($this->_config['path'], -1) !== DIRECTORY_SEPARATOR) { $this->_config['path'] .= DIRECTORY_SEPARATOR; } @@ -111,31 +118,38 @@ public function init(array $config = []): bool * for it or let the driver take care of that. * @return bool True on success and false on failure. */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { if ($value === '' || !$this->_init) { return false; } + $duration = $this->duration($ttl); $key = $this->_key($key); + $this->_eventClass = CacheBeforeSetEvent::class; + $this->dispatchEvent(CacheBeforeSetEvent::NAME, ['key' => $key, 'value' => $value, 'ttl' => $duration]); + $this->_eventClass = CacheAfterSetEvent::class; if ($this->_setKey($key, true) === false) { + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => false, 'ttl' => $duration, + ]); + return false; } + $origValue = $value; if (!empty($this->_config['serialize'])) { $value = serialize($value); } - $expires = time() + $this->duration($ttl); - $contents = implode([$expires, PHP_EOL, $value, PHP_EOL]); + $expires = time() + $duration; + $contents = implode('', [$expires, PHP_EOL, $value, PHP_EOL]); if ($this->_config['lock']) { - /** @psalm-suppress PossiblyNullReference */ $this->_File->flock(LOCK_EX); } - /** @psalm-suppress PossiblyNullReference */ $this->_File->rewind(); $success = $this->_File->ftruncate(0) && $this->_File->fwrite($contents) && @@ -144,7 +158,11 @@ public function set($key, $value, $ttl = null): bool if ($this->_config['lock']) { $this->_File->flock(LOCK_UN); } - $this->_File = null; + unset($this->_File); + + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $origValue, 'success' => $success, 'ttl' => $duration, + ]); return $success; } @@ -157,20 +175,23 @@ public function set($key, $value, $ttl = null): bool * @return mixed The cached data, or default value if the data doesn't exist, has * expired, or if there was an error fetching it */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { $key = $this->_key($key); + $this->_eventClass = CacheBeforeGetEvent::class; + $this->dispatchEvent(CacheBeforeGetEvent::NAME, ['key' => $key, 'default' => $default]); + $this->_eventClass = CacheAfterGetEvent::class; if (!$this->_init || $this->_setKey($key) === false) { + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); + return $default; } if ($this->_config['lock']) { - /** @psalm-suppress PossiblyNullReference */ $this->_File->flock(LOCK_SH); } - /** @psalm-suppress PossiblyNullReference */ $this->_File->rewind(); $time = time(); $cachetime = (int)$this->_File->current(); @@ -179,6 +200,7 @@ public function get($key, $default = null) if ($this->_config['lock']) { $this->_File->flock(LOCK_UN); } + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); return $default; } @@ -186,7 +208,6 @@ public function get($key, $default = null) $data = ''; $this->_File->next(); while ($this->_File->valid()) { - /** @psalm-suppress PossiblyInvalidOperand */ $data .= $this->_File->current(); $this->_File->next(); } @@ -199,8 +220,13 @@ public function get($key, $default = null) if ($data !== '' && !empty($this->_config['serialize'])) { $data = unserialize($data); + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $data, 'success' => true]); + + return $data; } + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $data, 'success' => true]); + return $data; } @@ -211,17 +237,29 @@ public function get($key, $default = null) * @return bool True if the value was successfully deleted, false if it didn't * exist or couldn't be removed */ - public function delete($key): bool + public function delete(string $key): bool { $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + $this->_eventClass = CacheAfterDeleteEvent::class; if ($this->_setKey($key) === false || !$this->_init) { + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => false]); + return false; } - /** @psalm-suppress PossiblyNullReference */ $path = $this->_File->getRealPath(); - $this->_File = null; + unset($this->_File); + + if ($path === false) { + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => false]); + + return false; + } + + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => true]); // phpcs:disable return @unlink($path); @@ -238,21 +276,21 @@ public function clear(): bool if (!$this->_init) { return false; } - $this->_File = null; + unset($this->_File); $this->_clearDirectory($this->_config['path']); $directory = new RecursiveDirectoryIterator( $this->_config['path'], - FilesystemIterator::SKIP_DOTS + FilesystemIterator::SKIP_DOTS, ); - $contents = new RecursiveIteratorIterator( + $iterator = new RecursiveIteratorIterator( $directory, - RecursiveIteratorIterator::SELF_FIRST + RecursiveIteratorIterator::SELF_FIRST, ); $cleared = []; /** @var \SplFileInfo $fileInfo */ - foreach ($contents as $fileInfo) { + foreach ($iterator as $fileInfo) { if ($fileInfo->isFile()) { unset($fileInfo); continue; @@ -276,7 +314,9 @@ public function clear(): bool // unsetting iterators helps releasing possible locks in certain environments, // which could otherwise make `rmdir()` fail - unset($directory, $contents); + unset($directory, $iterator); + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); return true; } @@ -307,7 +347,7 @@ protected function _clearDirectory(string $path): void try { $file = new SplFileObject($path . $entry, 'r'); - } catch (Exception $e) { + } catch (Exception) { continue; } @@ -332,7 +372,7 @@ protected function _clearDirectory(string $path): void * @return int|false * @throws \LogicException */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { throw new LogicException('Files cannot be atomically decremented.'); } @@ -345,7 +385,7 @@ public function decrement(string $key, int $offset = 1) * @return int|false * @throws \LogicException */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { throw new LogicException('Files cannot be atomically incremented.'); } @@ -367,7 +407,7 @@ protected function _setKey(string $key, bool $createKey = false): bool $dir = $this->_config['path'] . $groups; if (!is_dir($dir)) { - mkdir($dir, 0775, true); + mkdir($dir, $this->_config['dirMask'] ^ umask(), true); } $path = new SplFileInfo($dir . $key); @@ -376,7 +416,7 @@ protected function _setKey(string $key, bool $createKey = false): bool return false; } if ( - empty($this->_File) || + !isset($this->_File) || $this->_File->getBasename() !== $key || $this->_File->valid() === false ) { @@ -392,9 +432,9 @@ protected function _setKey(string $key, bool $createKey = false): bool if (!$exists && !chmod($this->_File->getPathname(), (int)$this->_config['mask'])) { trigger_error(sprintf( - 'Could not apply permission mask "%s" on cache file "%s"', + 'Could not apply permission mask `%s` on cache file `%s`', $this->_File->getPathname(), - $this->_config['mask'] + $this->_config['mask'], ), E_USER_WARNING); } } @@ -414,7 +454,7 @@ protected function _active(): bool $success = true; if (!is_dir($path)) { // phpcs:disable - $success = @mkdir($path, 0775, true); + $success = @mkdir($path, $this->_config['dirMask'] ^ umask(), true); // phpcs:enable } @@ -423,7 +463,7 @@ protected function _active(): bool $this->_init = false; trigger_error(sprintf( '%s is not writable', - $this->_config['path'] + $this->_config['path'], ), E_USER_WARNING); } @@ -433,18 +473,11 @@ protected function _active(): bool /** * @inheritDoc */ - protected function _key($key): string + protected function _key(string $key): string { $key = parent::_key($key); - if (preg_match('/[\/\\<>?:|*"]/', $key)) { - throw new InvalidArgumentException( - "Cache key `{$key}` contains invalid characters. " . - 'You cannot use /, \\, <, >, ?, :, |, *, or " in cache keys.' - ); - } - - return $key; + return rawurlencode($key); } /** @@ -455,14 +488,14 @@ protected function _key($key): string */ public function clearGroup(string $group): bool { - $this->_File = null; + unset($this->_File); $prefix = (string)$this->_config['prefix']; $directoryIterator = new RecursiveDirectoryIterator($this->_config['path']); $contents = new RecursiveIteratorIterator( $directoryIterator, - RecursiveIteratorIterator::CHILD_FIRST + RecursiveIteratorIterator::CHILD_FIRST, ); $filtered = new CallbackFilterIterator( $contents, @@ -471,20 +504,18 @@ function (SplFileInfo $current) use ($group, $prefix) { return false; } - $hasPrefix = $prefix === '' - || strpos($current->getBasename(), $prefix) === 0; + $hasPrefix = $prefix === '' || str_starts_with($current->getBasename(), $prefix); if ($hasPrefix === false) { return false; } - $pos = strpos( + return str_contains( $current->getPathname(), - DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR + DIRECTORY_SEPARATOR . $group . DIRECTORY_SEPARATOR, ); - - return $pos !== false; - } + }, ); + /** @var \SplFileInfo $object */ foreach ($filtered as $object) { $path = $object->getPathname(); unset($object); @@ -495,6 +526,8 @@ function (SplFileInfo $current) use ($group, $prefix) { // unsetting iterators helps releasing possible locks in certain environments, // which could otherwise make `rmdir()` fail unset($directoryIterator, $contents, $filtered); + $this->_eventClass = CacheGroupClearEvent::class; + $this->dispatchEvent(CacheGroupClearEvent::NAME, ['group' => $group]); return true; } diff --git a/Engine/MemcachedEngine.php b/Engine/MemcachedEngine.php index 781c4cfc7..edcbd88c5 100644 --- a/Engine/MemcachedEngine.php +++ b/Engine/MemcachedEngine.php @@ -17,9 +17,24 @@ namespace Cake\Cache\Engine; use Cake\Cache\CacheEngine; -use InvalidArgumentException; +use Cake\Cache\Event\CacheAfterAddEvent; +use Cake\Cache\Event\CacheAfterDecrementEvent; +use Cake\Cache\Event\CacheAfterDeleteEvent; +use Cake\Cache\Event\CacheAfterGetEvent; +use Cake\Cache\Event\CacheAfterIncrementEvent; +use Cake\Cache\Event\CacheAfterSetEvent; +use Cake\Cache\Event\CacheBeforeAddEvent; +use Cake\Cache\Event\CacheBeforeDecrementEvent; +use Cake\Cache\Event\CacheBeforeDeleteEvent; +use Cake\Cache\Event\CacheBeforeGetEvent; +use Cake\Cache\Event\CacheBeforeIncrementEvent; +use Cake\Cache\Event\CacheBeforeSetEvent; +use Cake\Cache\Event\CacheClearedEvent; +use Cake\Cache\Event\CacheGroupClearEvent; +use Cake\Cache\Exception\InvalidArgumentException; +use Cake\Core\Exception\CakeException; +use DateInterval; use Memcached; -use RuntimeException; /** * Memcached storage engine for cache. Memcached has some limitations in the amount of @@ -37,7 +52,7 @@ class MemcachedEngine extends CacheEngine * * @var \Memcached */ - protected $_Memcached; + protected Memcached $_Memcached; /** * The default config used unless overridden by runtime configuration @@ -53,16 +68,16 @@ class MemcachedEngine extends CacheEngine * - `prefix` Prepended to all entries. Good for when you need to share a keyspace * with either another cache config or another application. * - `serialize` The serializer engine used to serialize data. Available engines are 'php', - * 'igbinary' and 'json'. Beside 'php', the memcached extension must be compiled with the + * 'igbinary' and 'json'. Besides 'php', the memcached extension must be compiled with the * appropriate serializer support. * - `servers` String or array of memcached servers. If an array MemcacheEngine will use * them as a pool. * - `options` - Additional options for the memcached client. Should be an array of option => value. * Use the \Memcached::OPT_* constants as keys. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'compress' => false, 'duration' => 3600, 'groups' => [], @@ -82,29 +97,29 @@ class MemcachedEngine extends CacheEngine * * Memcached must be compiled with JSON and igbinary support to use these engines * - * @var array + * @var array */ - protected $_serializers = []; + protected array $_serializers = []; /** - * @var string[] + * @var array */ - protected $_compiledGroupNames = []; + protected array $_compiledGroupNames = []; /** * Initialize the Cache Engine * * Called automatically by the cache frontend * - * @param array $config array of setting for the engine + * @param array $config array of setting for the engine * @return bool True if the engine has been successfully initialized, false if not - * @throws \InvalidArgumentException When you try use authentication without + * @throws \Cake\Cache\Exception\InvalidArgumentException When you try use authentication without * Memcached compiled with SASL support */ public function init(array $config = []): bool { if (!extension_loaded('memcached')) { - throw new RuntimeException('The `memcached` extension must be enabled to use MemcachedEngine.'); + throw new CakeException('The `memcached` extension must be enabled to use MemcachedEngine.'); } $this->_serializers = [ @@ -134,7 +149,6 @@ public function init(array $config = []): bool $this->_config['servers'] = [$this->_config['servers']]; } - /** @psalm-suppress RedundantPropertyInitializationCheck */ if (isset($this->_Memcached)) { return true; } @@ -154,7 +168,7 @@ public function init(array $config = []): bool throw new InvalidArgumentException( 'Invalid cache configuration. Multiple persistent cache configurations are detected' . ' with different `servers` values. `servers` values for persistent cache configurations' . - ' must be the same when using the same persistence id.' + ' must be the same when using the same persistence id.', ); } } @@ -180,20 +194,21 @@ public function init(array $config = []): bool if (empty($this->_config['username']) && !empty($this->_config['login'])) { throw new InvalidArgumentException( - 'Please pass "username" instead of "login" for connecting to Memcached' + 'Please pass "username" instead of "login" for connecting to Memcached', ); } if ($this->_config['username'] !== null && $this->_config['password'] !== null) { + // @phpstan-ignore function.alreadyNarrowedType (check kept for SASL support detection) if (!method_exists($this->_Memcached, 'setSaslAuthData')) { throw new InvalidArgumentException( - 'Memcached extension is not built with SASL support' + 'Memcached extension is not built with SASL support', ); } $this->_Memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); $this->_Memcached->setSaslAuthData( $this->_config['username'], - $this->_config['password'] + $this->_config['password'], ); } @@ -204,7 +219,7 @@ public function init(array $config = []): bool * Settings the memcached instance * * @return void - * @throws \InvalidArgumentException When the Memcached extension is not built + * @throws \Cake\Cache\Exception\InvalidArgumentException When the Memcached extension is not built * with the desired serializer engine. */ protected function _setOptions(): void @@ -214,7 +229,7 @@ protected function _setOptions(): void $serializer = strtolower($this->_config['serialize']); if (!isset($this->_serializers[$serializer])) { throw new InvalidArgumentException( - sprintf('%s is not a valid serializer engine for Memcached', $serializer) + sprintf('`%s` is not a valid serializer engine for Memcached.', $serializer), ); } @@ -223,13 +238,13 @@ protected function _setOptions(): void !constant('Memcached::HAVE_' . strtoupper($serializer)) ) { throw new InvalidArgumentException( - sprintf('Memcached extension is not compiled with %s support', $serializer) + sprintf('Memcached extension is not compiled with `%s` support.', $serializer), ); } $this->_Memcached->setOption( Memcached::OPT_SERIALIZER, - $this->_serializers[$serializer] + $this->_serializers[$serializer], ); // Check for Amazon ElastiCache instance @@ -237,15 +252,12 @@ protected function _setOptions(): void defined('Memcached::OPT_CLIENT_MODE') && defined('Memcached::DYNAMIC_CLIENT_MODE') ) { - $this->_Memcached->setOption( - Memcached::OPT_CLIENT_MODE, - Memcached::DYNAMIC_CLIENT_MODE - ); + $this->_Memcached->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE); } $this->_Memcached->setOption( Memcached::OPT_COMPRESSION, - (bool)$this->_config['compress'] + (bool)$this->_config['compress'], ); } @@ -259,10 +271,10 @@ protected function _setOptions(): void public function parseServerString(string $server): array { $socketTransport = 'unix://'; - if (strpos($server, $socketTransport) === 0) { + if (str_starts_with($server, $socketTransport)) { return [substr($server, strlen($socketTransport)), 0]; } - if (substr($server, 0, 1) === '[') { + if (str_starts_with($server, '[')) { $position = strpos($server, ']:'); if ($position !== false) { $position++; @@ -287,7 +299,7 @@ public function parseServerString(string $server): array * @return string|int|bool|null * @see https://secure.php.net/manual/en/memcached.getoption.php */ - public function getOption(int $name) + public function getOption(int $name): string|int|bool|null { return $this->_Memcached->getOption($name); } @@ -306,11 +318,21 @@ public function getOption(int $name) * @return bool True if the data was successfully cached, false on failure * @see https://www.php.net/manual/en/memcached.set.php */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { + $key = $this->_key($key); $duration = $this->duration($ttl); + $this->_eventClass = CacheBeforeSetEvent::class; + $this->dispatchEvent(CacheBeforeSetEvent::NAME, ['key' => $key, 'value' => $value, 'ttl' => $duration]); + + $success = $this->_Memcached->set($key, $value, $duration); + + $this->_eventClass = CacheAfterSetEvent::class; + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); - return $this->_Memcached->set($this->_key($key), $value, $duration); + return $success; } /** @@ -322,7 +344,7 @@ public function set($key, $value, $ttl = null): bool * for it or let the driver take care of that. * @return bool Whether the write was successful or not. */ - public function setMultiple($values, $ttl = null): bool + public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool { $cacheData = []; foreach ($values as $key => $value) { @@ -341,26 +363,35 @@ public function setMultiple($values, $ttl = null): bool * @return mixed The cached data, or default value if the data doesn't exist, has * expired, or if there was an error fetching it. */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { $key = $this->_key($key); + $this->_eventClass = CacheBeforeGetEvent::class; + $this->dispatchEvent(CacheBeforeGetEvent::NAME, ['key' => $key, 'default' => $default]); + $value = $this->_Memcached->get($key); + + $this->_eventClass = CacheAfterGetEvent::class; if ($this->_Memcached->getResultCode() == Memcached::RES_NOTFOUND) { + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); + return $default; } + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $value, 'success' => true]); + return $value; } /** * Read many keys from the cache at once * - * @param iterable $keys An array of identifiers for the data + * @param iterable $keys An array of identifiers for the data * @param mixed $default Default value to return for keys that do not exist. - * @return array An array containing, for each of the given $keys, the cached data or - * false if cached data could not be retrieved. + * @return iterable An array containing, for each of the given $keys, the cached data or + * `$default` if cached data could not be retrieved. */ - public function getMultiple($keys, $default = null): array + public function getMultiple(iterable $keys, mixed $default = null): iterable { $cacheKeys = []; foreach ($keys as $key) { @@ -368,9 +399,13 @@ public function getMultiple($keys, $default = null): array } $values = $this->_Memcached->getMulti($cacheKeys); + if ($values === false) { + return array_fill_keys(array_keys($cacheKeys), $default); + } + $return = []; foreach ($cacheKeys as $original => $prefixed) { - $return[$original] = $values[$prefixed] ?? $default; + $return[$original] = array_key_exists($prefixed, $values) ? $values[$prefixed] : $default; } return $return; @@ -383,9 +418,20 @@ public function getMultiple($keys, $default = null): array * @param int $offset How much to increment * @return int|false New incremented value, false otherwise */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { - return $this->_Memcached->increment($this->_key($key), $offset); + $key = $this->_key($key); + $this->_eventClass = CacheBeforeIncrementEvent::class; + $this->dispatchEvent(CacheBeforeIncrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + + $value = $this->_Memcached->increment($key, $offset); + + $this->_eventClass = CacheAfterIncrementEvent::class; + $this->dispatchEvent(CacheAfterIncrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $value !== false, 'value' => $value, + ]); + + return $value; } /** @@ -395,9 +441,20 @@ public function increment(string $key, int $offset = 1) * @param int $offset How much to subtract * @return int|false New decremented value, false otherwise */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { - return $this->_Memcached->decrement($this->_key($key), $offset); + $key = $this->_key($key); + $this->_eventClass = CacheBeforeDecrementEvent::class; + $this->dispatchEvent(CacheBeforeDecrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + + $value = $this->_Memcached->decrement($key, $offset); + + $this->_eventClass = CacheAfterDecrementEvent::class; + $this->dispatchEvent(CacheAfterDecrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $value !== false, 'value' => $value, + ]); + + return $value; } /** @@ -407,9 +464,18 @@ public function decrement(string $key, int $offset = 1) * @return bool True if the value was successfully deleted, false if it didn't * exist or couldn't be removed. */ - public function delete($key): bool + public function delete(string $key): bool { - return $this->_Memcached->delete($this->_key($key)); + $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + + $success = $this->_Memcached->delete($key); + + $this->_eventClass = CacheAfterDeleteEvent::class; + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => $success]); + + return $success; } /** @@ -419,14 +485,21 @@ public function delete($key): bool * @return bool of boolean values that are true if the key was successfully * deleted, false if it didn't exist or couldn't be removed. */ - public function deleteMultiple($keys): bool + public function deleteMultiple(iterable $keys): bool { $cacheKeys = []; + $this->_eventClass = CacheBeforeDeleteEvent::class; foreach ($keys as $key) { $cacheKeys[] = $this->_key($key); + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + } + $success = (bool)$this->_Memcached->deleteMulti($cacheKeys); + $this->_eventClass = CacheAfterDeleteEvent::class; + foreach ($cacheKeys as $key) { + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => $success]); } - return (bool)$this->_Memcached->deleteMulti($cacheKeys); + return $success; } /** @@ -442,10 +515,12 @@ public function clear(): bool } foreach ($keys as $key) { - if (strpos($key, $this->_config['prefix']) === 0) { + if (str_starts_with($key, $this->_config['prefix'])) { $this->_Memcached->delete($key); } } + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); return true; } @@ -457,12 +532,22 @@ public function clear(): bool * @param mixed $value Data to be cached. * @return bool True if the data was successfully cached, false on failure. */ - public function add(string $key, $value): bool + public function add(string $key, mixed $value): bool { $duration = $this->_config['duration']; $key = $this->_key($key); - return $this->_Memcached->add($key, $value, $duration); + $this->_eventClass = CacheBeforeAddEvent::class; + $this->dispatchEvent(CacheBeforeAddEvent::NAME, ['key' => $key, 'value' => $value, 'ttl' => $duration]); + + $success = $this->_Memcached->add($key, $value, $duration); + + $this->_eventClass = CacheAfterAddEvent::class; + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); + + return $success; } /** @@ -470,11 +555,11 @@ public function add(string $key, $value): bool * If the group initial value was not found, then it initializes * the group accordingly. * - * @return string[] + * @return array */ public function groups(): array { - if (empty($this->_compiledGroupNames)) { + if (!$this->_compiledGroupNames) { foreach ($this->_config['groups'] as $group) { $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; } @@ -509,6 +594,10 @@ public function groups(): array */ public function clearGroup(string $group): bool { - return (bool)$this->_Memcached->increment($this->_config['prefix'] . $group); + $result = (bool)$this->_Memcached->increment($this->_config['prefix'] . $group); + $this->_eventClass = CacheGroupClearEvent::class; + $this->dispatchEvent(CacheGroupClearEvent::NAME, ['group' => $group]); + + return $result; } } diff --git a/Engine/NullEngine.php b/Engine/NullEngine.php index 4612a276b..4fc45ee6a 100644 --- a/Engine/NullEngine.php +++ b/Engine/NullEngine.php @@ -17,6 +17,7 @@ namespace Cake\Cache\Engine; use Cake\Cache\CacheEngine; +use DateInterval; /** * Null cache engine, all operations appear to work, but do nothing. @@ -36,7 +37,7 @@ public function init(array $config = []): bool /** * @inheritDoc */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { return true; } @@ -44,7 +45,7 @@ public function set($key, $value, $ttl = null): bool /** * @inheritDoc */ - public function setMultiple($values, $ttl = null): bool + public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool { return true; } @@ -52,7 +53,7 @@ public function setMultiple($values, $ttl = null): bool /** * @inheritDoc */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { return $default; } @@ -60,15 +61,21 @@ public function get($key, $default = null) /** * @inheritDoc */ - public function getMultiple($keys, $default = null): iterable + public function getMultiple(iterable $keys, mixed $default = null): iterable { - return []; + $result = []; + + foreach ($keys as $key) { + $result[$key] = $default; + } + + return $result; } /** * @inheritDoc */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { return 1; } @@ -76,7 +83,7 @@ public function increment(string $key, int $offset = 1) /** * @inheritDoc */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { return 0; } @@ -84,7 +91,7 @@ public function decrement(string $key, int $offset = 1) /** * @inheritDoc */ - public function delete($key): bool + public function delete(string $key): bool { return true; } @@ -92,7 +99,7 @@ public function delete($key): bool /** * @inheritDoc */ - public function deleteMultiple($keys): bool + public function deleteMultiple(iterable $keys): bool { return true; } diff --git a/Engine/RedisEngine.php b/Engine/RedisEngine.php index 1eeed8cad..943234a9b 100644 --- a/Engine/RedisEngine.php +++ b/Engine/RedisEngine.php @@ -18,10 +18,28 @@ namespace Cake\Cache\Engine; use Cake\Cache\CacheEngine; +use Cake\Cache\Event\CacheAfterAddEvent; +use Cake\Cache\Event\CacheAfterDecrementEvent; +use Cake\Cache\Event\CacheAfterDeleteEvent; +use Cake\Cache\Event\CacheAfterGetEvent; +use Cake\Cache\Event\CacheAfterIncrementEvent; +use Cake\Cache\Event\CacheAfterSetEvent; +use Cake\Cache\Event\CacheBeforeAddEvent; +use Cake\Cache\Event\CacheBeforeDecrementEvent; +use Cake\Cache\Event\CacheBeforeDeleteEvent; +use Cake\Cache\Event\CacheBeforeGetEvent; +use Cake\Cache\Event\CacheBeforeIncrementEvent; +use Cake\Cache\Event\CacheBeforeSetEvent; +use Cake\Cache\Event\CacheClearedEvent; +use Cake\Cache\Event\CacheGroupClearEvent; +use Cake\Core\Exception\CakeException; use Cake\Log\Log; +use DateInterval; +use Generator; use Redis; +use RedisCluster; +use RedisClusterException; use RedisException; -use RuntimeException; /** * Redis storage engine for cache. @@ -33,11 +51,12 @@ class RedisEngine extends CacheEngine * * @var \Redis */ - protected $_Redis; + protected Redis|RedisCluster $_Redis; /** * The default config used unless overridden by runtime configuration * + * - `clusterName` Redis cluster name * - `database` database number to use for connection. * - `duration` Specify how long items in this cache configuration last. * - `groups` List of groups or 'tags' associated to every key stored in this config. @@ -45,26 +64,48 @@ class RedisEngine extends CacheEngine * - `password` Redis server password. * - `persistent` Connect to the Redis server with a persistent connection * - `port` port number to the Redis server. + * - `tls` connect to the Redis server using TLS. * - `prefix` Prefix appended to all entries. Good for when you need to share a keyspace * with either another cache config or another application. + * - `scanCount` Number of keys to ask for each scan (default: 10) * - `server` URL or IP to the Redis server host. * - `timeout` timeout in seconds (float). * - `unix_socket` Path to the unix socket file (default: false) + * - `readTimeout` Read timeout in seconds (float). + * - `nodes` When using redis-cluster, the URL or IP addresses of the + * Redis cluster nodes. + * Format: an array of strings in the form `:`, like: + * [ + * ':', + * ':', + * ':', + * ] + * - `failover` Failover mode (distribute,distribute_slaves,error,none). Cluster mode only. + * - `clearUsesFlushDb` Enable clear() and clearBlocking() to use FLUSHDB. This will be + * faster than standard clear()/clearBlocking() but will ignore prefixes and will + * cause dataloss if other applications are sharing a redis database. * - * @var array + * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ + 'clusterName' => null, 'database' => 0, 'duration' => 3600, 'groups' => [], 'password' => false, 'persistent' => true, 'port' => 6379, + 'tls' => false, 'prefix' => 'cake_', 'host' => null, 'server' => '127.0.0.1', 'timeout' => 0, 'unix_socket' => false, + 'scanCount' => 10, + 'readTimeout' => 0, + 'nodes' => [], + 'failover' => null, + 'clearUsesFlushDb' => false, ]; /** @@ -72,13 +113,13 @@ class RedisEngine extends CacheEngine * * Called automatically by the cache frontend * - * @param array $config array of setting for the engine + * @param array $config array of setting for the engine * @return bool True if the engine has been successfully initialized, false if not */ public function init(array $config = []): bool { if (!extension_loaded('redis')) { - throw new RuntimeException('The `redis` extension must be enabled to use RedisEngine.'); + throw new CakeException('The `redis` extension must be enabled to use RedisEngine.'); } if (!empty($config['host'])) { @@ -97,24 +138,119 @@ public function init(array $config = []): bool */ protected function _connect(): bool { + if (!empty($this->_config['nodes']) || !empty($this->_config['clusterName'])) { + return $this->connectRedisCluster(); + } + + return $this->connectRedis(); + } + + /** + * Connects to a Redis cluster server + * + * @return bool True if Redis server was connected + */ + protected function connectRedisCluster(): bool + { + $connected = false; + + if (empty($this->_config['nodes'])) { + // @codeCoverageIgnoreStart + if (class_exists(Log::class)) { + Log::error('RedisEngine requires one or more nodes in cluster mode'); + } + // @codeCoverageIgnoreEnd + + return false; + } + + // @codeCoverageIgnoreStart + $ssl = []; + if ($this->_config['tls']) { + $map = [ + 'ssl_ca' => 'cafile', + 'ssl_key' => 'local_pk', + 'ssl_cert' => 'local_cert', + 'verify_peer' => 'verify_peer', + 'verify_peer_name' => 'verify_peer_name', + 'allow_self_signed' => 'allow_self_signed', + ]; + + foreach ($map as $configKey => $sslOption) { + if (array_key_exists($configKey, $this->_config)) { + $ssl[$sslOption] = $this->_config[$configKey]; + } + } + } + // @codeCoverageIgnoreEnd + + try { + $this->_Redis = new RedisCluster( + $this->_config['clusterName'], + $this->_config['nodes'], + (float)$this->_config['timeout'], + (float)$this->_config['readTimeout'], + $this->_config['persistent'], + $this->_config['password'], + $this->_config['tls'] ? ['ssl' => $ssl] : null, // @codeCoverageIgnore + ); + + $connected = true; + } catch (RedisClusterException $e) { + $connected = false; + + // @codeCoverageIgnoreStart + if (class_exists(Log::class)) { + Log::error('RedisEngine could not connect to the redis cluster. Got error: ' . $e->getMessage()); + } + // @codeCoverageIgnoreEnd + } + + $failover = match ($this->_config['failover']) { + RedisCluster::FAILOVER_DISTRIBUTE, 'distribute' => RedisCluster::FAILOVER_DISTRIBUTE, + RedisCluster::FAILOVER_DISTRIBUTE_SLAVES, 'distribute_slaves' => RedisCluster::FAILOVER_DISTRIBUTE_SLAVES, + RedisCluster::FAILOVER_ERROR, 'error' => RedisCluster::FAILOVER_ERROR, + RedisCluster::FAILOVER_NONE, 'none' => RedisCluster::FAILOVER_NONE, + default => null, + }; + + if ($failover !== null) { + $this->_Redis->setOption(RedisCluster::OPT_SLAVE_FAILOVER, $failover); + } + + return $connected; + } + + /** + * Connects to a Redis server + * + * @return bool True if Redis server was connected + */ + protected function connectRedis(): bool + { + $tls = $this->_config['tls'] === true ? 'tls://' : ''; + + $map = [ + 'ssl_ca' => 'cafile', + 'ssl_key' => 'local_pk', + 'ssl_cert' => 'local_cert', + ]; + + $ssl = []; + foreach ($map as $key => $context) { + if (!empty($this->_config[$key])) { + $ssl[$context] = $this->_config[$key]; + } + } + try { - $this->_Redis = new Redis(); + $this->_Redis = $this->_createRedisInstance(); if (!empty($this->_config['unix_socket'])) { $return = $this->_Redis->connect($this->_config['unix_socket']); } elseif (empty($this->_config['persistent'])) { - $return = $this->_Redis->connect( - $this->_config['server'], - (int)$this->_config['port'], - (int)$this->_config['timeout'] - ); + $return = $this->_connectTransient($tls . $this->_config['server'], $ssl); } else { - $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; - $return = $this->_Redis->pconnect( - $this->_config['server'], - (int)$this->_config['port'], - (int)$this->_config['timeout'], - $persistentId - ); + $return = $this->_connectPersistent($tls . $this->_config['server'], $ssl); } } catch (RedisException $e) { if (class_exists(Log::class)) { @@ -123,16 +259,78 @@ protected function _connect(): bool return false; } + if ($return && $this->_config['password']) { $return = $this->_Redis->auth($this->_config['password']); } if ($return) { - $return = $this->_Redis->select((int)$this->_config['database']); + return $this->_Redis->select((int)$this->_config['database']); } return $return; } + /** + * Connects to a Redis server using a new connection. + * + * @param string $server Server to connect to. + * @param array $ssl SSL context options. + * @throws \RedisException + * @return bool True if Redis server was connected + */ + protected function _connectTransient(string $server, array $ssl): bool + { + if ($ssl === []) { + return $this->_Redis->connect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + ); + } + + return $this->_Redis->connect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + null, + 0, + 0.0, + ['ssl' => $ssl], + ); + } + + /** + * Connects to a Redis server using a persistent connection. + * + * @param string $server Server to connect to. + * @param array $ssl SSL context options. + * @throws \RedisException + * @return bool True if Redis server was connected + */ + protected function _connectPersistent(string $server, array $ssl): bool + { + $persistentId = $this->_config['port'] . $this->_config['timeout'] . $this->_config['database']; + + if ($ssl === []) { + return $this->_Redis->pconnect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + $persistentId, + ); + } + + return $this->_Redis->pconnect( + $server, + (int)$this->_config['port'], + (int)$this->_config['timeout'], + $persistentId, + 0, + 0.0, + ['ssl' => $ssl], + ); + } + /** * Write data for key into cache. * @@ -143,17 +341,30 @@ protected function _connect(): bool * for it or let the driver take care of that. * @return bool True if the data was successfully cached, false on failure */ - public function set($key, $value, $ttl = null): bool + public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool { $key = $this->_key($key); $value = $this->serialize($value); - $duration = $this->duration($ttl); + $this->_eventClass = CacheBeforeSetEvent::class; + $this->dispatchEvent(CacheBeforeSetEvent::NAME, ['key' => $key, 'value' => $value, 'ttl' => $duration]); + + $this->_eventClass = CacheAfterSetEvent::class; if ($duration === 0) { - return $this->_Redis->set($key, $value); + $success = $this->_Redis->set($key, $value); + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); + + return $success; } - return $this->_Redis->setEx($key, $duration, $value); + $success = $this->_Redis->setEx($key, $duration, $value); + $this->dispatchEvent(CacheAfterSetEvent::NAME, [ + 'key' => $key, 'value' => $value, 'success' => $success, 'ttl' => $duration, + ]); + + return $success; } /** @@ -164,14 +375,35 @@ public function set($key, $value, $ttl = null): bool * @return mixed The cached data, or the default if the data doesn't exist, has * expired, or if there was an error fetching it */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { - $value = $this->_Redis->get($this->_key($key)); + $key = $this->_key($key); + $this->_eventClass = CacheBeforeGetEvent::class; + $this->dispatchEvent(CacheBeforeGetEvent::NAME, ['key' => $key, 'default' => $default]); + + $value = $this->_Redis->get($key); + + $this->_eventClass = CacheAfterGetEvent::class; if ($value === false) { + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => null, 'success' => false]); + return $default; } - return $this->unserialize($value); + $data = $this->unserialize($value); + $this->dispatchEvent(CacheAfterGetEvent::NAME, ['key' => $key, 'value' => $value, 'success' => true]); + + return $data; + } + + /** + * @inheritDoc + */ + public function has(string $key): bool + { + $res = $this->_Redis->exists($this->_key($key)); + + return is_int($res) ? $res > 0 : $res === true; } /** @@ -181,12 +413,20 @@ public function get($key, $default = null) * @param int $offset How much to increment * @return int|false New incremented value, false otherwise */ - public function increment(string $key, int $offset = 1) + public function increment(string $key, int $offset = 1): int|false { $duration = $this->_config['duration']; $key = $this->_key($key); + $this->_eventClass = CacheBeforeIncrementEvent::class; + $this->dispatchEvent(CacheBeforeIncrementEvent::NAME, ['key' => $key, 'offset' => $offset]); + $value = $this->_Redis->incrBy($key, $offset); + + $this->_eventClass = CacheAfterIncrementEvent::class; + $this->dispatchEvent(CacheAfterIncrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $value !== false, 'value' => $value, + ]); if ($duration > 0) { $this->_Redis->expire($key, $duration); } @@ -201,12 +441,19 @@ public function increment(string $key, int $offset = 1) * @param int $offset How much to subtract * @return int|false New decremented value, false otherwise */ - public function decrement(string $key, int $offset = 1) + public function decrement(string $key, int $offset = 1): int|false { $duration = $this->_config['duration']; $key = $this->_key($key); + $this->_eventClass = CacheBeforeDecrementEvent::class; + $this->dispatchEvent(CacheBeforeDecrementEvent::NAME, ['key' => $key, 'offset' => $offset]); $value = $this->_Redis->decrBy($key, $offset); + + $this->_eventClass = CacheAfterDecrementEvent::class; + $this->dispatchEvent(CacheAfterDecrementEvent::NAME, [ + 'key' => $key, 'offset' => $offset, 'success' => $value !== false, 'value' => $value, + ]); if ($duration > 0) { $this->_Redis->expire($key, $duration); } @@ -220,11 +467,41 @@ public function decrement(string $key, int $offset = 1) * @param string $key Identifier for the data * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed */ - public function delete($key): bool + public function delete(string $key): bool { $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); - return $this->_Redis->del($key) > 0; + $success = (int)$this->_Redis->del($key) > 0; + + $this->_eventClass = CacheAfterDeleteEvent::class; + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => $success]); + + return $success; + } + + /** + * Delete a key from the cache asynchronously + * + * Just unlink a key from the cache. The actual removal will happen later asynchronously. + * + * @param string $key Identifier for the data + * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed + */ + public function deleteAsync(string $key): bool + { + $key = $this->_key($key); + $this->_eventClass = CacheBeforeDeleteEvent::class; + $this->dispatchEvent(CacheBeforeDeleteEvent::NAME, ['key' => $key]); + + $result = $this->_Redis->unlink($key); + $success = is_int($result) && $result > 0; + + $this->_eventClass = CacheAfterDeleteEvent::class; + $this->dispatchEvent(CacheAfterDeleteEvent::NAME, ['key' => $key, 'success' => $success]); + + return $success; } /** @@ -234,25 +511,54 @@ public function delete($key): bool */ public function clear(): bool { - $this->_Redis->setOption(Redis::OPT_SCAN, (string)Redis::SCAN_RETRY); + if ($this->getConfig('clearUsesFlushDb')) { + $this->flushDB(true); + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); + + return true; + } $isAllDeleted = true; - $iterator = null; $pattern = $this->_config['prefix'] . '*'; - while (true) { - $keys = $this->_Redis->scan($iterator, $pattern); + foreach ($this->scanKeys($pattern) as $key) { + $result = $this->_Redis->unlink($key); + $isDeleted = is_int($result) && $result > 0; + $isAllDeleted = $isAllDeleted && $isDeleted; + } + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); - if ($keys === false) { - break; - } + return $isAllDeleted; + } - foreach ($keys as $key) { - $isDeleted = ($this->_Redis->del($key) > 0); - $isAllDeleted = $isAllDeleted && $isDeleted; - } + /** + * Delete all keys from the cache by a blocking operation + * + * @return bool True if the cache was successfully cleared, false otherwise + */ + public function clearBlocking(): bool + { + if ($this->getConfig('clearUsesFlushDb')) { + $this->flushDB(false); + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); + + return true; } + $isAllDeleted = true; + $pattern = $this->_config['prefix'] . '*'; + + foreach ($this->scanKeys($pattern) as $key) { + // Blocking delete + $isDeleted = ((int)$this->_Redis->del($key) > 0); + $isAllDeleted = $isAllDeleted && $isDeleted; + } + $this->_eventClass = CacheClearedEvent::class; + $this->dispatchEvent(CacheClearedEvent::NAME); + return $isAllDeleted; } @@ -265,15 +571,29 @@ public function clear(): bool * @return bool True if the data was successfully cached, false on failure. * @link https://github.com/phpredis/phpredis#set */ - public function add(string $key, $value): bool + public function add(string $key, mixed $value): bool { $duration = $this->_config['duration']; $key = $this->_key($key); + $origValue = $value; $value = $this->serialize($value); + $this->_eventClass = CacheBeforeAddEvent::class; + $this->dispatchEvent(CacheBeforeAddEvent::NAME, [ + 'key' => $key, 'value' => $origValue, 'ttl' => $duration, + ]); + + $this->_eventClass = CacheAfterAddEvent::class; if ($this->_Redis->set($key, $value, ['nx', 'ex' => $duration])) { + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $key, 'value' => $origValue, 'success' => true, 'ttl' => $duration, + ]); + return true; } + $this->dispatchEvent(CacheAfterAddEvent::NAME, [ + 'key' => $key, 'value' => $origValue, 'success' => false, 'ttl' => $duration, + ]); return false; } @@ -283,7 +603,7 @@ public function add(string $key, $value): bool * If the group initial value was not found, then it initializes * the group accordingly. * - * @return string[] + * @return array */ public function groups(): array { @@ -309,20 +629,24 @@ public function groups(): array */ public function clearGroup(string $group): bool { - return (bool)$this->_Redis->incr($this->_config['prefix'] . $group); + $success = (bool)$this->_Redis->incr($this->_config['prefix'] . $group); + $this->_eventClass = CacheGroupClearEvent::class; + $this->dispatchEvent(CacheGroupClearEvent::NAME, ['group' => $group]); + + return $success; } /** * Serialize value for saving to Redis. * * This is needed instead of using Redis' in built serialization feature - * as it creates problems incrementing/decrementing intially set integer value. + * as it creates problems incrementing/decrementing initially set integer value. * * @param mixed $value Value to serialize. * @return string * @link https://github.com/phpredis/phpredis/issues/81 */ - protected function serialize($value): string + protected function serialize(mixed $value): string { if (is_int($value)) { return (string)$value; @@ -337,7 +661,7 @@ protected function serialize($value): string * @param string $value Value to unserialize. * @return mixed */ - protected function unserialize(string $value) + protected function unserialize(string $value): mixed { if (preg_match('/^[-]?\d+$/', $value)) { return (int)$value; @@ -346,12 +670,81 @@ protected function unserialize(string $value) return unserialize($value); } + /** + * Create new Redis instance. + * + * @return \Redis + */ + protected function _createRedisInstance(): Redis + { + return new Redis(); + } + + /** + * Unifies Redis and RedisCluster scan() calls and simplifies its use. + * + * @param string $pattern Pattern to scan + * @return \Generator + */ + private function scanKeys(string $pattern): Generator + { + $this->_Redis->setOption(Redis::OPT_SCAN, (string)Redis::SCAN_RETRY); + + if ($this->_Redis instanceof RedisCluster) { + foreach ($this->_Redis->_masters() as $node) { + $iterator = null; + while (true) { + $keys = $this->_Redis->scan($iterator, $node, $pattern, (int)$this->_config['scanCount']); + if ($keys === false) { + break; + } + + if (is_array($keys)) { + foreach ($keys as $key) { + yield $key; + } + } + } + } + } else { + $iterator = null; + while (true) { + $keys = $this->_Redis->scan($iterator, $pattern, (int)$this->_config['scanCount']); + if ($keys === false) { + break; + } + + foreach ($keys as $key) { + yield $key; + } + } + } + } + + /** + * Flushes DB + * + * @param bool $async Whether to use asynchronous mode + * @return void + */ + private function flushDB(bool $async): void + { + if ($this->_Redis instanceof RedisCluster) { + foreach ($this->_Redis->_masters() as $node) { + // @phpstan-ignore arguments.count + $this->_Redis->flushDB($node, $async); + } + } else { + $this->_Redis->flushDB($async); + } + } + /** * Disconnects from the redis server */ public function __destruct() { - if (empty($this->_config['persistent']) && $this->_Redis instanceof Redis) { + if (isset($this->_Redis) && empty($this->_config['persistent'])) { $this->_Redis->close(); } } diff --git a/Engine/WincacheEngine.php b/Engine/WincacheEngine.php deleted file mode 100644 index 9d9e36eb6..000000000 --- a/Engine/WincacheEngine.php +++ /dev/null @@ -1,202 +0,0 @@ -_key($key); - $duration = $this->duration($ttl); - - return wincache_ucache_set($key, $value, $duration); - } - - /** - * Read a key from the cache - * - * @param string $key Identifier for the data - * @param mixed $default Default value to return if the key does not exist. - * @return mixed The cached data, or default value if the data doesn't exist, - * has expired, or if there was an error fetching it - */ - public function get($key, $default = null) - { - $value = wincache_ucache_get($this->_key($key), $success); - if ($success === false) { - return $default; - } - - return $value; - } - - /** - * Increments the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to increment - * @return int|false New incremented value, false otherwise - */ - public function increment(string $key, int $offset = 1) - { - $key = $this->_key($key); - - return wincache_ucache_inc($key, $offset); - } - - /** - * Decrements the value of an integer cached key - * - * @param string $key Identifier for the data - * @param int $offset How much to subtract - * @return int|false New decremented value, false otherwise - */ - public function decrement(string $key, int $offset = 1) - { - $key = $this->_key($key); - - return wincache_ucache_dec($key, $offset); - } - - /** - * Delete a key from the cache - * - * @param string $key Identifier for the data - * @return bool True if the value was successfully deleted, false if it didn't exist or couldn't be removed - */ - public function delete($key): bool - { - $key = $this->_key($key); - - return wincache_ucache_delete($key); - } - - /** - * Delete all keys from the cache. This will clear every - * item in the cache matching the cache config prefix. - * - * @return bool True Returns true. - */ - public function clear(): bool - { - $info = wincache_ucache_info(); - $cacheKeys = $info['ucache_entries']; - unset($info); - foreach ($cacheKeys as $key) { - if (strpos($key['key_name'], $this->_config['prefix']) === 0) { - wincache_ucache_delete($key['key_name']); - } - } - - return true; - } - - /** - * Returns the `group value` for each of the configured groups - * If the group initial value was not found, then it initializes - * the group accordingly. - * - * @return string[] - */ - public function groups(): array - { - if (empty($this->_compiledGroupNames)) { - foreach ($this->_config['groups'] as $group) { - $this->_compiledGroupNames[] = $this->_config['prefix'] . $group; - } - } - - $groups = wincache_ucache_get($this->_compiledGroupNames); - if (count($groups) !== count($this->_config['groups'])) { - foreach ($this->_compiledGroupNames as $group) { - if (!isset($groups[$group])) { - wincache_ucache_set($group, 1); - $groups[$group] = 1; - } - } - ksort($groups); - } - - $result = []; - $groups = array_values($groups); - foreach ($this->_config['groups'] as $i => $group) { - $result[] = $group . $groups[$i]; - } - - return $result; - } - - /** - * Increments the group value to simulate deletion of all keys under a group - * old values will remain in storage until they expire. - * - * @param string $group The group to clear. - * @return bool success - */ - public function clearGroup(string $group): bool - { - $success = false; - wincache_ucache_inc($this->_config['prefix'] . $group, 1, $success); - - return $success; - } -} diff --git a/Event/CacheAfterAddEvent.php b/Event/CacheAfterAddEvent.php new file mode 100644 index 000000000..768709483 --- /dev/null +++ b/Event/CacheAfterAddEvent.php @@ -0,0 +1,118 @@ + + */ +class CacheAfterAddEvent extends Event +{ + public const NAME = 'Cache.afterAdd'; + + protected string $key; + + protected mixed $value = null; + + protected DateInterval|int|null $ttl = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + if (isset($data['ttl'])) { + $this->ttl = $data['ttl']; + unset($data['ttl']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * @return \DateInterval|int|null + */ + public function getTtl(): DateInterval|int|null + { + return $this->ttl; + } +} diff --git a/Event/CacheAfterDecrementEvent.php b/Event/CacheAfterDecrementEvent.php new file mode 100644 index 000000000..aaea59faa --- /dev/null +++ b/Event/CacheAfterDecrementEvent.php @@ -0,0 +1,123 @@ + + */ +class CacheAfterDecrementEvent extends Event +{ + public const NAME = 'Cache.afterDecrement'; + + protected string $key; + + protected int $offset; + + protected mixed $value; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['offset'])) { + $this->offset = $data['offset']; + unset($data['offset']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * Get the cache key. + * + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get the decrement offset. + * + * @return int + */ + public function getOffset(): int + { + return $this->offset; + } + + /** + * Get the new value after decrement. + * + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/Event/CacheAfterDeleteEvent.php b/Event/CacheAfterDeleteEvent.php new file mode 100644 index 000000000..11cb9a2dc --- /dev/null +++ b/Event/CacheAfterDeleteEvent.php @@ -0,0 +1,89 @@ + + */ +class CacheAfterDeleteEvent extends Event +{ + public const NAME = 'Cache.afterDelete'; + + protected string $key; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } +} diff --git a/Event/CacheAfterGetEvent.php b/Event/CacheAfterGetEvent.php new file mode 100644 index 000000000..a3860d4b3 --- /dev/null +++ b/Event/CacheAfterGetEvent.php @@ -0,0 +1,103 @@ + + */ +class CacheAfterGetEvent extends Event +{ + public const NAME = 'Cache.afterGet'; + + protected string $key; + + protected mixed $value = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/Event/CacheAfterIncrementEvent.php b/Event/CacheAfterIncrementEvent.php new file mode 100644 index 000000000..47b726064 --- /dev/null +++ b/Event/CacheAfterIncrementEvent.php @@ -0,0 +1,123 @@ + + */ +class CacheAfterIncrementEvent extends Event +{ + public const NAME = 'Cache.afterIncrement'; + + protected string $key; + + protected int $offset; + + protected mixed $value; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['offset'])) { + $this->offset = $data['offset']; + unset($data['offset']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * Get the cache key. + * + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get the increment offset. + * + * @return int + */ + public function getOffset(): int + { + return $this->offset; + } + + /** + * Get the new value after increment. + * + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } +} diff --git a/Event/CacheAfterSetEvent.php b/Event/CacheAfterSetEvent.php new file mode 100644 index 000000000..950dda4d4 --- /dev/null +++ b/Event/CacheAfterSetEvent.php @@ -0,0 +1,118 @@ + + */ +class CacheAfterSetEvent extends Event +{ + public const NAME = 'Cache.afterSet'; + + protected string $key; + + protected mixed $value = null; + + protected DateInterval|int|null $ttl = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['success'])) { + $this->result = $data['success']; + unset($data['success']); + } + if (isset($data['ttl'])) { + $this->ttl = $data['ttl']; + unset($data['ttl']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * The result value of the event listeners + * + * @return bool|null + */ + public function getResult(): ?bool + { + return $this->result; + } + + /** + * Listeners can attach a result value to the event. + * + * @param mixed $value The value to set. + * @return $this + */ + public function setResult(mixed $value = null) + { + if ($value !== null && !is_bool($value)) { + throw new InvalidArgumentException( + 'The result for CacheEngine events must be a `bool`.', + ); + } + + return parent::setResult($value); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * @return \DateInterval|int|null + */ + public function getTtl(): DateInterval|int|null + { + return $this->ttl; + } +} diff --git a/Event/CacheBeforeAddEvent.php b/Event/CacheBeforeAddEvent.php new file mode 100644 index 000000000..bb7efe71a --- /dev/null +++ b/Event/CacheBeforeAddEvent.php @@ -0,0 +1,86 @@ + + */ +class CacheBeforeAddEvent extends Event +{ + public const NAME = 'Cache.beforeAdd'; + + protected string $key; + + protected mixed $value = null; + + protected DateInterval|int|null $ttl = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['ttl'])) { + $this->ttl = $data['ttl']; + unset($data['ttl']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * @return \DateInterval|int|null + */ + public function getTtl(): DateInterval|int|null + { + return $this->ttl; + } +} diff --git a/Event/CacheBeforeDecrementEvent.php b/Event/CacheBeforeDecrementEvent.php new file mode 100644 index 000000000..5e95dc6df --- /dev/null +++ b/Event/CacheBeforeDecrementEvent.php @@ -0,0 +1,75 @@ + + */ +class CacheBeforeDecrementEvent extends Event +{ + public const NAME = 'Cache.beforeDecrement'; + + protected string $key; + + protected int $offset; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['offset'])) { + $this->offset = $data['offset']; + unset($data['offset']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * Get the cache key. + * + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get the decrement offset. + * + * @return int + */ + public function getOffset(): int + { + return $this->offset; + } +} diff --git a/Event/CacheBeforeDeleteEvent.php b/Event/CacheBeforeDeleteEvent.php new file mode 100644 index 000000000..7d63f668c --- /dev/null +++ b/Event/CacheBeforeDeleteEvent.php @@ -0,0 +1,57 @@ + + */ +class CacheBeforeDeleteEvent extends Event +{ + public const NAME = 'Cache.beforeDelete'; + + protected string $key; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } +} diff --git a/Event/CacheBeforeGetEvent.php b/Event/CacheBeforeGetEvent.php new file mode 100644 index 000000000..8c29e21a3 --- /dev/null +++ b/Event/CacheBeforeGetEvent.php @@ -0,0 +1,71 @@ + + */ +class CacheBeforeGetEvent extends Event +{ + public const NAME = 'Cache.beforeGet'; + + protected string $key; + + protected mixed $default = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['default'])) { + $this->default = $data['default']; + unset($data['default']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getDefault(): mixed + { + return $this->default; + } +} diff --git a/Event/CacheBeforeIncrementEvent.php b/Event/CacheBeforeIncrementEvent.php new file mode 100644 index 000000000..b167b1212 --- /dev/null +++ b/Event/CacheBeforeIncrementEvent.php @@ -0,0 +1,75 @@ + + */ +class CacheBeforeIncrementEvent extends Event +{ + public const NAME = 'Cache.beforeIncrement'; + + protected string $key; + + protected int $offset; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['offset'])) { + $this->offset = $data['offset']; + unset($data['offset']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * Get the cache key. + * + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * Get the increment offset. + * + * @return int + */ + public function getOffset(): int + { + return $this->offset; + } +} diff --git a/Event/CacheBeforeSetEvent.php b/Event/CacheBeforeSetEvent.php new file mode 100644 index 000000000..99306f926 --- /dev/null +++ b/Event/CacheBeforeSetEvent.php @@ -0,0 +1,86 @@ + + */ +class CacheBeforeSetEvent extends Event +{ + public const NAME = 'Cache.beforeSet'; + + protected string $key; + + protected mixed $value = null; + + protected DateInterval|int|null $ttl = null; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['key'])) { + $this->key = $data['key']; + unset($data['key']); + } + if (isset($data['value'])) { + $this->value = $data['value']; + unset($data['value']); + } + if (isset($data['ttl'])) { + $this->ttl = $data['ttl']; + unset($data['ttl']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return mixed + */ + public function getValue(): mixed + { + return $this->value; + } + + /** + * @return \DateInterval|int|null + */ + public function getTtl(): DateInterval|int|null + { + return $this->ttl; + } +} diff --git a/Event/CacheClearedEvent.php b/Event/CacheClearedEvent.php new file mode 100644 index 000000000..362a87c42 --- /dev/null +++ b/Event/CacheClearedEvent.php @@ -0,0 +1,29 @@ + + */ +class CacheClearedEvent extends Event +{ + public const NAME = 'Cache.cleared'; +} diff --git a/Event/CacheGroupClearEvent.php b/Event/CacheGroupClearEvent.php new file mode 100644 index 000000000..fcd792269 --- /dev/null +++ b/Event/CacheGroupClearEvent.php @@ -0,0 +1,59 @@ + + */ +class CacheGroupClearEvent extends Event +{ + public const NAME = 'Cache.clearedGroup'; + + protected string $group; + + /** + * Constructor + * + * @param string $name Name of the event + * @param \Cake\Cache\CacheEngine $subject The Cache engine instance this event applies to. + * @param array $data Any value you wish to be transported with this event to it can be read by listeners. + */ + public function __construct(string $name, CacheEngine $subject, array $data = []) + { + if (isset($data['group'])) { + $this->group = $data['group']; + unset($data['group']); + } + + parent::__construct($name, $subject, $data); + } + + /** + * Get the group name + * + * @return string + */ + public function getGroup(): string + { + return $this->group; + } +} diff --git a/Exception/CacheWriteException.php b/Exception/CacheWriteException.php new file mode 100644 index 000000000..241ef6539 --- /dev/null +++ b/Exception/CacheWriteException.php @@ -0,0 +1,27 @@ +=7.2.0", - "cakephp/core": "^4.0", - "psr/simple-cache": "^1.0.0" + "php": ">=8.2", + "cakephp/core": "5.3.*@dev", + "cakephp/event": "5.3.*@dev", + "psr/simple-cache": "^2.0 || ^3.0" }, "provide": { - "psr/simple-cache-implementation": "^1.0.0" + "psr/simple-cache-implementation": "^3.0" }, "autoload": { "psr-4": { "Cake\\Cache\\": "." } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "extra": { + "branch-alias": { + "dev-5.next": "5.3.x-dev" + } } }