isSerializationRequired($storage)) { throw new SimpleCacheException(sprintf( 'The storage adapter "%s" requires a serializer plugin; please see' . ' https://docs.laminas.dev/laminas-cache/storage/plugin/#quick-start' . ' for details on how to attach the plugin to your adapter.', get_class($storage) )); } $capabilities = $storage->getCapabilities(); $this->memoizeTtlCapabilities($capabilities); $this->memoizeMaximumKeyLengthCapability($storage, $capabilities); $this->storage = $storage; $this->utc = new DateTimeZone('UTC'); } /** * {@inheritDoc} */ public function get($key, $default = null) { $this->validateKey($key); $this->success = null; try { $result = $this->storage->getItem($key, $this->success); } catch (Throwable $throwable) { throw static::translateThrowable($throwable); } $result = $result === null ? $default : $result; return $this->success ? $result : $default; } /** * {@inheritDoc} */ public function set($key, $value, $ttl = null) { $this->validateKey($key); $ttl = $this->convertTtlToInteger($ttl); // PSR-16 states that 0 or negative TTL values should result in cache // invalidation for the item. if (null !== $ttl && 1 > $ttl) { return $this->delete($key); } // If a positive TTL is set, but the adapter does not support per-item // TTL, we return false immediately. if (null !== $ttl && ! $this->providesPerItemTtl) { return false; } $options = $this->storage->getOptions(); $previousTtl = $options->getTtl(); if ($ttl !== null) { $options->setTtl($ttl); } try { $result = $this->storage->setItem($key, $value); } catch (Throwable $throwable) { throw static::translateThrowable($throwable); } finally { $options->setTtl($previousTtl); } return $result; } /** * {@inheritDoc} */ public function delete($key) { $this->validateKey($key); try { return null !== $this->storage->removeItem($key); } catch (Throwable $throwable) { return false; } } /** * {@inheritDoc} */ public function clear() { $namespace = $this->storage->getOptions()->getNamespace(); if ($this->storage instanceof ClearByNamespaceInterface && $namespace) { return $this->storage->clearByNamespace($namespace); } if ($this->storage instanceof FlushableInterface) { return $this->storage->flush(); } return false; } /** * {@inheritDoc} */ public function getMultiple($keys, $default = null) { $keys = $this->convertIterableToArray($keys, false, __FUNCTION__); array_walk($keys, [$this, 'validateKey']); try { $results = $this->storage->getItems($keys); } catch (Throwable $throwable) { throw static::translateThrowable($throwable); } foreach ($keys as $key) { if (! isset($results[$key])) { $results[$key] = $default; continue; } } return $results; } /** * {@inheritDoc} */ public function setMultiple($values, $ttl = null) { $values = $this->convertIterableToArray($values, true, __FUNCTION__); $keys = array_keys($values); $ttl = $this->convertTtlToInteger($ttl); // PSR-16 states that 0 or negative TTL values should result in cache // invalidation for the items. if (null !== $ttl && 1 > $ttl) { return $this->deleteMultiple($keys); } array_walk($keys, [$this, 'validateKey']); // If a positive TTL is set, but the adapter does not support per-item // TTL, we return false -- but not until after we validate keys. if (null !== $ttl && ! $this->providesPerItemTtl) { return false; } $options = $this->storage->getOptions(); $previousTtl = $options->getTtl(); if ($ttl !== null) { $options->setTtl($ttl); } try { $result = $this->storage->setItems($values); } catch (Throwable $throwable) { throw static::translateThrowable($throwable); } finally { $options->setTtl($previousTtl); } if (empty($result)) { return true; } foreach ($result as $index => $key) { if (! $this->storage->hasItem($key)) { unset($result[$index]); } } return empty($result); } /** * {@inheritDoc} */ public function deleteMultiple($keys) { $keys = $this->convertIterableToArray($keys, false, __FUNCTION__); if (empty($keys)) { return true; } array_walk($keys, [$this, 'validateKey']); try { $result = $this->storage->removeItems($keys); } catch (Throwable $throwable) { return false; } if (empty($result)) { return true; } foreach ($result as $index => $key) { if (! $this->storage->hasItem($key)) { unset($result[$index]); } } return empty($result); } /** * {@inheritDoc} */ public function has($key) { $this->validateKey($key); try { return $this->storage->hasItem($key); } catch (Throwable $throwable) { throw static::translateThrowable($throwable); } } private static function translateThrowable(Throwable $throwable): PsrCacheExceptionInterface { $exceptionClass = $throwable instanceof LaminasCacheInvalidArgumentException ? SimpleCacheInvalidArgumentException::class : SimpleCacheException::class; return new $exceptionClass($throwable->getMessage(), $throwable->getCode(), $throwable); } /** * @param string $key * @return void * @throws SimpleCacheInvalidArgumentException if key is invalid */ private function validateKey($key) { if ('' === $key) { throw new SimpleCacheInvalidArgumentException( 'Invalid key provided; cannot be empty' ); } if (0 === $key) { // cache/integration-tests erroneously tests that ['0' => 'value'] // is a valid payload to setMultiple(). However, PHP silently // converts '0' to 0, which would normally be invalid. For now, // we need to catch just this single value so tests pass. // I have filed an issue to correct the test: // https://github.com/php-cache/integration-tests/issues/92 return $key; } if (! is_string($key)) { throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid key provided of type "%s"%s; must be a string', is_object($key) ? get_class($key) : gettype($key), is_scalar($key) ? sprintf(' (%s)', var_export($key, true)) : '' )); } $regex = sprintf('/[%s]/', preg_quote(self::INVALID_KEY_CHARS, '/')); if (preg_match($regex, $key)) { throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid key "%s" provided; cannot contain any of (%s)', $key, self::INVALID_KEY_CHARS )); } if ($this->maximumKeyLength !== Capabilities::UNLIMITED_KEY_LENGTH && preg_match('/^.{'.($this->maximumKeyLength + 1).',}/u', $key) ) { throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid key "%s" provided; key is too long. Must be no more than %d characters', $key, $this->maximumKeyLength )); } } /** * Determine if the storage adapter provides per-item TTL capabilities */ private function memoizeTtlCapabilities(Capabilities $capabilities): void { $this->providesPerItemTtl = $capabilities->getStaticTtl() && (0 < $capabilities->getMinTtl()); } /** * @param int|DateInterval * @return null|int * @throws SimpleCacheInvalidArgumentException for invalid arguments */ private function convertTtlToInteger($ttl) { // null === absence of a TTL if (null === $ttl) { return null; } // integers are always okay if (is_int($ttl)) { return $ttl; } // Numeric strings evaluating to integers can be cast if (is_string($ttl) && ('0' === $ttl || preg_match('/^[1-9][0-9]+$/', $ttl) ) ) { return (int) $ttl; } // DateIntervals require conversion if ($ttl instanceof DateInterval) { $now = new DateTimeImmutable('now', $this->utc); $end = $now->add($ttl); return $end->getTimestamp() - $now->getTimestamp(); } // All others are invalid throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid TTL "%s" provided; must be null, an integer, or a %s instance', is_object($ttl) ? get_class($ttl) : var_export($ttl, true), DateInterval::class )); } /** * @param array|iterable $iterable * @param bool $useKeys Whether or not to preserve keys during conversion * @param string $forMethod Method that called this one; used for reporting * invalid values. * @return array * @throws SimpleCacheInvalidArgumentException for invalid $iterable values */ private function convertIterableToArray($iterable, $useKeys, $forMethod) { if (is_array($iterable)) { return $iterable; } if (! $iterable instanceof Traversable) { throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid value provided to %s::%s; must be an array or Traversable', __CLASS__, $forMethod )); } $array = []; foreach ($iterable as $key => $value) { if (! $useKeys) { $array[] = $value; continue; } if (! is_string($key) && ! is_int($key) && ! is_float($key)) { throw new SimpleCacheInvalidArgumentException(sprintf( 'Invalid key detected of type "%s"; must be a scalar', is_object($key) ? get_class($key) : gettype($key) )); } $array[$key] = $value; } return $array; } private function memoizeMaximumKeyLengthCapability(StorageInterface $storage, Capabilities $capabilities): void { $maximumKeyLength = $capabilities->getMaxKeyLength(); if ($maximumKeyLength === Capabilities::UNLIMITED_KEY_LENGTH) { $this->maximumKeyLength = Capabilities::UNLIMITED_KEY_LENGTH; return; } if ($maximumKeyLength === Capabilities::UNKNOWN_KEY_LENGTH) { // For backward compatibility, assume adapters which do not provide a maximum key length do support 64 chars $maximumKeyLength = 64; } if ($maximumKeyLength < 64) { throw new SimpleCacheInvalidArgumentException(sprintf( 'The storage adapter "%s" does not fulfill the minimum requirements for PSR-16:' .' The maximum key length capability must allow at least 64 characters.', get_class($storage) )); } $this->maximumKeyLength = min($maximumKeyLength, self::PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1); } }