* @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved * @license LICENSE MIT */ class SimpleCache implements CacheInterface { /** * List of invalid (or reserved) key characters. * * @var string */ /* public */ const KEY_INVALID_CHARACTERS = '{}()/\@:'; /** * @var KeyValueStore */ protected $store; public function __construct(KeyValueStore $store) { $this->store = $store; } /** * {@inheritdoc} */ public function get($key, $default = null) { $this->assertValidKey($key); // KeyValueStore::get returns false for cache misses (which could also // be confused for a `false` value), so we'll check existence with getMulti $multi = $this->store->getMulti(array($key)); return isset($multi[$key]) ? $multi[$key] : $default; } /** * {@inheritdoc} */ public function set($key, $value, $ttl = null) { $this->assertValidKey($key); $ttl = $this->ttl($ttl); return $this->store->set($key, $value, $ttl); } /** * {@inheritdoc} */ public function delete($key) { $this->assertValidKey($key); $this->store->delete($key); // as long as the item is gone from the cache (even if it never existed // and delete failed because of that), we should return `true` return true; } /** * {@inheritdoc} */ public function clear() { return $this->store->flush(); } /** * {@inheritdoc} */ public function getMultiple($keys, $default = null) { if ($keys instanceof \Traversable) { $keys = iterator_to_array($keys, false); } if (!is_array($keys)) { throw new InvalidArgumentException('Invalid keys: '.var_export($keys, true).'. Keys should be an array or Traversable of strings.'); } array_map(array($this, 'assertValidKey'), $keys); $results = $this->store->getMulti($keys); // KeyValueStore omits values that are not in cache, while PSR-16 will // have them with a default value $nulls = array_fill_keys($keys, $default); $results = array_merge($nulls, $results); return $results; } /** * {@inheritdoc} */ public function setMultiple($values, $ttl = null) { if ($values instanceof \Traversable) { // we also need the keys, and an array is stricter about what it can // have as keys than a Traversable is, so we can't use // iterator_to_array... $array = array(); foreach ($values as $key => $value) { if (!is_string($key) && !is_int($key)) { throw new InvalidArgumentException('Invalid values: '.var_export($values, true).'. Only strings are allowed as keys.'); } $array[$key] = $value; } $values = $array; } if (!is_array($values)) { throw new InvalidArgumentException('Invalid values: '.var_export($values, true).'. Values should be an array or Traversable with strings as keys.'); } foreach ($values as $key => $value) { // $key is also allowed to be an integer, since ['0' => ...] will // automatically convert to [0 => ...] $key = is_int($key) ? (string) $key : $key; $this->assertValidKey($key); } $ttl = $this->ttl($ttl); $success = $this->store->setMulti($values, $ttl); return !in_array(false, $success); } /** * {@inheritdoc} */ public function deleteMultiple($keys) { if ($keys instanceof \Traversable) { $keys = iterator_to_array($keys, false); } if (!is_array($keys)) { throw new InvalidArgumentException('Invalid keys: '.var_export($keys, true).'. Keys should be an array or Traversable of strings.'); } array_map(array($this, 'assertValidKey'), $keys); $this->store->deleteMulti($keys); // as long as the item is gone from the cache (even if it never existed // and delete failed because of that), we should return `true` return true; } /** * {@inheritdoc} */ public function has($key) { $this->assertValidKey($key); // KeyValueStore::get returns false for cache misses (which could also // be confused for a `false` value), so we'll check existence with getMulti $multi = $this->store->getMulti(array($key)); return isset($multi[$key]); } /** * Throws an exception if $key is invalid. * * @param string $key * * @throws InvalidArgumentException */ protected function assertValidKey($key) { if (!is_string($key)) { throw new InvalidArgumentException('Invalid key: '.var_export($key, true).'. Key should be a string.'); } if ('' === $key) { throw new InvalidArgumentException('Invalid key. Key should not be empty.'); } // valid key according to PSR-16 rules $invalid = preg_quote(static::KEY_INVALID_CHARACTERS, '/'); if (preg_match('/['.$invalid.']/', $key)) { throw new InvalidArgumentException('Invalid key: '.$key.'. Contains (a) character(s) reserved for future extension: '.static::KEY_INVALID_CHARACTERS); } } /** * Accepts all TTL inputs valid in PSR-16 (null|int|DateInterval) and * converts them into TTL for KeyValueStore (int). * * @param int|\DateInterval|null $ttl * * @return int * * @throws \TypeError */ protected function ttl($ttl) { if (null === $ttl) { return 0; } elseif (is_int($ttl)) { /* * PSR-16 specifies that if `0` is provided, it must be treated as * expired, whereas KeyValueStore will interpret 0 to mean "never * expire". */ if (0 === $ttl) { return -1; } /* * PSR-16 only accepts relative timestamps, whereas KeyValueStore * accepts both relative & absolute, depending on what the timestamp * is. We'll convert all timestamps > 30 days into absolute * timestamps; the others can remain relative, as KeyValueStore will * already treat those values as such. * @see https://github.com/dragoonis/psr-simplecache/issues/3 */ if ($ttl > 30 * 24 * 60 * 60) { return $ttl + time(); } return $ttl; } elseif ($ttl instanceof \DateInterval) { // convert DateInterval to integer by adding it to a 0 DateTime $datetime = new \DateTime(); $datetime->setTimestamp(0); $datetime->add($ttl); return time() + (int) $datetime->format('U'); } $error = 'Invalid time: '.serialize($ttl).'. Must be integer or '. 'instance of DateInterval.'; if (class_exists('\TypeError')) { throw new \TypeError($error); } trigger_error($error, E_USER_ERROR); return 0; } }