3 * @see https://github.com/zendframework/zend-cache for the canonical source repository
4 * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License
8 namespace Zend\Cache\Psr\SimpleCache
;
11 use DateTimeImmutable
;
14 use Psr\SimpleCache\CacheInterface
as SimpleCacheInterface
;
17 use Zend\Cache\Exception\InvalidArgumentException
as ZendCacheInvalidArgumentException
;
18 use Zend\Cache\Psr\SerializationTrait
;
19 use Zend\Cache\Storage\ClearByNamespaceInterface
;
20 use Zend\Cache\Storage\FlushableInterface
;
21 use Zend\Cache\Storage\StorageInterface
;
24 * Decorate a zend-cache storage adapter for usage as a PSR-16 implementation.
26 class SimpleCacheDecorator
implements SimpleCacheInterface
28 use SerializationTrait
;
31 * Characters reserved by PSR-16 that are not valid in cache keys.
33 const INVALID_KEY_CHARS
= ':@{}()/\\';
38 private $providesPerItemTtl = true;
41 * @var StorageInterface
46 * Reference used by storage when calling getItem() to indicate status of
58 public function __construct(StorageInterface
$storage)
60 if ($this->isSerializationRequired($storage)) {
61 throw new SimpleCacheException(sprintf(
62 'The storage adapter "%s" requires a serializer plugin; please see'
63 . ' https://docs.zendframework.com/zend-cache/storage/plugin/#quick-start'
64 . ' for details on how to attach the plugin to your adapter.',
69 $this->memoizeTtlCapabilities($storage);
70 $this->storage
= $storage;
71 $this->utc
= new DateTimeZone('UTC');
77 public function get($key, $default = null)
79 $this->validateKey($key);
81 $this->success
= null;
83 $result = $this->storage
->getItem($key, $this->success
);
84 } catch (Throwable
$e) {
85 throw static::translateException($e);
86 } catch (Exception
$e) {
87 throw static::translateException($e);
90 $result = $result === null ?
$default : $result;
91 return $this->success ?
$result : $default;
97 public function set($key, $value, $ttl = null)
99 $this->validateKey($key);
100 $ttl = $this->convertTtlToInteger($ttl);
102 // PSR-16 states that 0 or negative TTL values should result in cache
103 // invalidation for the item.
104 if (null !== $ttl && 1 > $ttl) {
105 return $this->delete($key);
108 // If a positive TTL is set, but the adapter does not support per-item
109 // TTL, we return false immediately.
110 if (null !== $ttl && ! $this->providesPerItemTtl
) {
114 $options = $this->storage
->getOptions();
115 $previousTtl = $options->getTtl();
116 $options->setTtl($ttl);
119 $result = $this->storage
->setItem($key, $value);
120 } catch (Throwable
$e) {
121 throw static::translateException($e);
122 } catch (Exception
$e) {
123 throw static::translateException($e);
125 $options->setTtl($previousTtl);
134 public function delete($key)
136 $this->validateKey($key);
139 return null !== $this->storage
->removeItem($key);
140 } catch (Throwable
$e) {
142 } catch (Exception
$e) {
150 public function clear()
152 $namespace = $this->storage
->getOptions()->getNamespace();
154 if ($this->storage
instanceof ClearByNamespaceInterface
&& $namespace) {
155 return $this->storage
->clearByNamespace($namespace);
158 if ($this->storage
instanceof FlushableInterface
) {
159 return $this->storage
->flush();
168 public function getMultiple($keys, $default = null)
170 $keys = $this->convertIterableToArray($keys, false, __FUNCTION__
);
171 array_walk($keys, [$this, 'validateKey']);
174 $results = $this->storage
->getItems($keys);
175 } catch (Throwable
$e) {
176 throw static::translateException($e);
177 } catch (Exception
$e) {
178 throw static::translateException($e);
181 foreach ($keys as $key) {
182 if (! isset($results[$key])) {
183 $results[$key] = $default;
194 public function setMultiple($values, $ttl = null)
196 $values = $this->convertIterableToArray($values, true, __FUNCTION__
);
197 $keys = array_keys($values);
198 $ttl = $this->convertTtlToInteger($ttl);
200 // PSR-16 states that 0 or negative TTL values should result in cache
201 // invalidation for the items.
202 if (null !== $ttl && 1 > $ttl) {
203 return $this->deleteMultiple($keys);
206 array_walk($keys, [$this, 'validateKey']);
208 // If a positive TTL is set, but the adapter does not support per-item
209 // TTL, we return false -- but not until after we validate keys.
210 if (null !== $ttl && ! $this->providesPerItemTtl
) {
214 $options = $this->storage
->getOptions();
215 $previousTtl = $options->getTtl();
216 $options->setTtl($ttl);
219 $result = $this->storage
->setItems($values);
220 } catch (Throwable
$e) {
221 throw static::translateException($e);
222 } catch (Exception
$e) {
223 throw static::translateException($e);
225 $options->setTtl($previousTtl);
228 if (empty($result)) {
232 foreach ($result as $index => $key) {
233 if (! $this->storage
->hasItem($key)) {
234 unset($result[$index]);
238 return empty($result);
244 public function deleteMultiple($keys)
246 $keys = $this->convertIterableToArray($keys, false, __FUNCTION__
);
251 array_walk($keys, [$this, 'validateKey']);
254 $result = $this->storage
->removeItems($keys);
255 } catch (Throwable
$e) {
257 } catch (Exception
$e) {
261 if (empty($result)) {
265 foreach ($result as $index => $key) {
266 if (! $this->storage
->hasItem($key)) {
267 unset($result[$index]);
271 return empty($result);
277 public function has($key)
279 $this->validateKey($key);
282 return $this->storage
->hasItem($key);
283 } catch (Throwable
$e) {
284 throw static::translateException($e);
285 } catch (Exception
$e) {
286 throw static::translateException($e);
291 * @param Throwable|Exception $e
292 * @return SimpleCacheException
294 private static function translateException($e)
296 $exceptionClass = $e instanceof ZendCacheInvalidArgumentException
297 ? SimpleCacheInvalidArgumentException
::class
298 : SimpleCacheException
::class;
300 return new $exceptionClass($e->getMessage(), $e->getCode(), $e);
306 * @throws SimpleCacheInvalidArgumentException if key is invalid
308 private function validateKey($key)
311 throw new SimpleCacheInvalidArgumentException(
312 'Invalid key provided; cannot be empty'
317 // cache/integration-tests erroneously tests that ['0' => 'value']
318 // is a valid payload to setMultiple(). However, PHP silently
319 // converts '0' to 0, which would normally be invalid. For now,
320 // we need to catch just this single value so tests pass.
321 // I have filed an issue to correct the test:
322 // https://github.com/php-cache/integration-tests/issues/92
326 if (! is_string($key)) {
327 throw new SimpleCacheInvalidArgumentException(sprintf(
328 'Invalid key provided of type "%s"%s; must be a string',
329 is_object($key) ?
get_class($key) : gettype($key),
330 is_scalar($key) ?
sprintf(' (%s)', var_export($key, true)) : ''
334 $regex = sprintf('/[%s]/', preg_quote(self
::INVALID_KEY_CHARS
, '/'));
335 if (preg_match($regex, $key)) {
336 throw new SimpleCacheInvalidArgumentException(sprintf(
337 'Invalid key "%s" provided; cannot contain any of (%s)',
339 self
::INVALID_KEY_CHARS
343 if (preg_match('/^.{65,}/u', $key)) {
344 throw new SimpleCacheInvalidArgumentException(sprintf(
345 'Invalid key "%s" provided; key is too long. Must be no more than 64 characters',
352 * Determine if the storage adapter provides per-item TTL capabilities
354 * @param StorageInterface $storage
357 private function memoizeTtlCapabilities(StorageInterface
$storage)
359 $capabilities = $storage->getCapabilities();
360 $this->providesPerItemTtl
= $capabilities->getStaticTtl() && (0 < $capabilities->getMinTtl());
364 * @param int|DateInterval
366 * @throws SimpleCacheInvalidArgumentException for invalid arguments
368 private function convertTtlToInteger($ttl)
370 // null === absence of a TTL
375 // integers are always okay
380 // Numeric strings evaluating to integers can be cast
383 ||
preg_match('/^[1-9][0-9]+$/', $ttl)
389 // DateIntervals require conversion
390 if ($ttl instanceof DateInterval
) {
391 $now = new DateTimeImmutable('now', $this->utc
);
392 $end = $now->add($ttl);
393 return $end->getTimestamp() - $now->getTimestamp();
396 // All others are invalid
397 throw new SimpleCacheInvalidArgumentException(sprintf(
398 'Invalid TTL "%s" provided; must be null, an integer, or a %s instance',
399 is_object($ttl) ?
get_class($ttl) : var_export($ttl, true),
405 * @param array|iterable $iterable
406 * @param bool $useKeys Whether or not to preserve keys during conversion
407 * @param string $forMethod Method that called this one; used for reporting
410 * @throws SimpleCacheInvalidArgumentException for invalid $iterable values
412 private function convertIterableToArray($iterable, $useKeys, $forMethod)
414 if (is_array($iterable)) {
418 if (! $iterable instanceof Traversable
) {
419 throw new SimpleCacheInvalidArgumentException(sprintf(
420 'Invalid value provided to %s::%s; must be an array or Traversable',
427 foreach ($iterable as $key => $value) {
433 if (! is_string($key) && ! is_int($key) && ! is_float($key)) {
434 throw new SimpleCacheInvalidArgumentException(sprintf(
435 'Invalid key detected of type "%s"; must be a scalar',
436 is_object($key) ?
get_class($key) : gettype($key)
439 $array[$key] = $value;