You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

559 lines
13 KiB

<?php
/*
* This file is part of php-cache organization.
*
* (c) 2015 Aaron Scherer <aequasi@gmail.com>, Tobias Nyholm <tobias.nyholm@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Cache\Adapter\Common;
use Cache\Adapter\Common\Exception\CacheException;
use Cache\Adapter\Common\Exception\CachePoolException;
use Cache\Adapter\Common\Exception\InvalidArgumentException;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
/**
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
abstract class AbstractCachePool implements PhpCachePool, LoggerAwareInterface, CacheInterface
{
const SEPARATOR_TAG = '!';
/**
* @type LoggerInterface
*/
private $logger;
/**
* @type PhpCacheItem[] deferred
*/
protected $deferred = [];
/**
* @param PhpCacheItem $item
* @param int|null $ttl seconds from now
*
* @return bool true if saved
*/
abstract protected function storeItemInCache(PhpCacheItem $item, $ttl);
/**
* Fetch an object from the cache implementation.
*
* If it is a cache miss, it MUST return [false, null, [], null]
*
* @param string $key
*
* @return array with [isHit, value, tags[], expirationTimestamp]
*/
abstract protected function fetchObjectFromCache($key);
/**
* Clear all objects from cache.
*
* @return bool false if error
*/
abstract protected function clearAllObjectsFromCache();
/**
* Remove one object from cache.
*
* @param string $key
*
* @return bool
*/
abstract protected function clearOneObjectFromCache($key);
/**
* Get an array with all the values in the list named $name.
*
* @param string $name
*
* @return array
*/
abstract protected function getList($name);
/**
* Remove the list.
*
* @param string $name
*
* @return bool
*/
abstract protected function removeList($name);
/**
* Add a item key on a list named $name.
*
* @param string $name
* @param string $key
*/
abstract protected function appendListItem($name, $key);
/**
* Remove an item from the list.
*
* @param string $name
* @param string $key
*/
abstract protected function removeListItem($name, $key);
/**
* Make sure to commit before we destruct.
*/
public function __destruct()
{
$this->commit();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$this->validateKey($key);
if (isset($this->deferred[$key])) {
/** @type CacheItem $item */
$item = clone $this->deferred[$key];
$item->moveTagsToPrevious();
return $item;
}
$func = function () use ($key) {
try {
return $this->fetchObjectFromCache($key);
} catch (\Exception $e) {
$this->handleException($e, __FUNCTION__);
}
};
return new CacheItem($key, $func);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
$items = [];
foreach ($keys as $key) {
$items[$key] = $this->getItem($key);
}
return $items;
}
/**
* {@inheritdoc}
*/
public function hasItem($key)
{
try {
return $this->getItem($key)->isHit();
} catch (\Exception $e) {
$this->handleException($e, __FUNCTION__);
}
}
/**
* {@inheritdoc}
*/
public function clear()
{
// Clear the deferred items
$this->deferred = [];
try {
return $this->clearAllObjectsFromCache();
} catch (\Exception $e) {
$this->handleException($e, __FUNCTION__);
}
}
/**
* {@inheritdoc}
*/
public function deleteItem($key)
{
try {
return $this->deleteItems([$key]);
} catch (\Exception $e) {
$this->handleException($e, __FUNCTION__);
}
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys)
{
$deleted = true;
foreach ($keys as $key) {
$this->validateKey($key);
// Delete form deferred
unset($this->deferred[$key]);
// We have to commit here to be able to remove deferred hierarchy items
$this->commit();
$this->preRemoveItem($key);
if (!$this->clearOneObjectFromCache($key)) {
$deleted = false;
}
}
return $deleted;
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item)
{
if (!$item instanceof PhpCacheItem) {
$e = new InvalidArgumentException('Cache items are not transferable between pools. Item MUST implement PhpCacheItem.');
$this->handleException($e, __FUNCTION__);
}
$this->removeTagEntries($item);
$this->saveTags($item);
$timeToLive = null;
if (null !== $timestamp = $item->getExpirationTimestamp()) {
$timeToLive = $timestamp - time();
if ($timeToLive < 0) {
return $this->deleteItem($item->getKey());
}
}
try {
return $this->storeItemInCache($item, $timeToLive);
} catch (\Exception $e) {
$this->handleException($e, __FUNCTION__);
}
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item)
{
$this->deferred[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*/
public function commit()
{
$saved = true;
foreach ($this->deferred as $item) {
if (!$this->save($item)) {
$saved = false;
}
}
$this->deferred = [];
return $saved;
}
/**
* @param string $key
*
* @throws InvalidArgumentException
*/
protected function validateKey($key)
{
if (!is_string($key)) {
$e = new InvalidArgumentException(sprintf(
'Cache key must be string, "%s" given',
gettype($key)
));
$this->handleException($e, __FUNCTION__);
}
if (!isset($key[0])) {
$e = new InvalidArgumentException('Cache key cannot be an empty string');
$this->handleException($e, __FUNCTION__);
}
if (preg_match('|[\{\}\(\)/\\\@\:]|', $key)) {
$e = new InvalidArgumentException(sprintf(
'Invalid key: "%s". The key contains one or more characters reserved for future extension: {}()/\@:',
$key
));
$this->handleException($e, __FUNCTION__);
}
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* Logs with an arbitrary level if the logger exists.
*
* @param mixed $level
* @param string $message
* @param array $context
*/
protected function log($level, $message, array $context = [])
{
if ($this->logger !== null) {
$this->logger->log($level, $message, $context);
}
}
/**
* Log exception and rethrow it.
*
* @param \Exception $e
* @param string $function
*
* @throws CachePoolException
*/
private function handleException(\Exception $e, $function)
{
$level = 'alert';
if ($e instanceof InvalidArgumentException) {
$level = 'warning';
}
$this->log($level, $e->getMessage(), ['exception' => $e]);
if (!$e instanceof CacheException) {
$e = new CachePoolException(sprintf('Exception thrown when executing "%s". ', $function), 0, $e);
}
throw $e;
}
/**
* @param array $tags
*
* @return bool
*/
public function invalidateTags(array $tags)
{
$itemIds = [];
foreach ($tags as $tag) {
$itemIds = array_merge($itemIds, $this->getList($this->getTagKey($tag)));
}
// Remove all items with the tag
$success = $this->deleteItems($itemIds);
if ($success) {
// Remove the tag list
foreach ($tags as $tag) {
$this->removeList($this->getTagKey($tag));
$l = $this->getList($this->getTagKey($tag));
}
}
return $success;
}
public function invalidateTag($tag)
{
return $this->invalidateTags([$tag]);
}
/**
* @param PhpCacheItem $item
*/
protected function saveTags(PhpCacheItem $item)
{
$tags = $item->getTags();
foreach ($tags as $tag) {
$this->appendListItem($this->getTagKey($tag), $item->getKey());
}
}
/**
* Removes the key form all tag lists. When an item with tags is removed
* we MUST remove the tags. If we fail to remove the tags a new item with
* the same key will automatically get the previous tags.
*
* @param string $key
*
* @return $this
*/
protected function preRemoveItem($key)
{
$item = $this->getItem($key);
$this->removeTagEntries($item);
return $this;
}
/**
* @param PhpCacheItem $item
*/
private function removeTagEntries(PhpCacheItem $item)
{
$tags = $item->getPreviousTags();
foreach ($tags as $tag) {
$this->removeListItem($this->getTagKey($tag), $item->getKey());
}
}
/**
* @param string $tag
*
* @return string
*/
protected function getTagKey($tag)
{
return 'tag'.self::SEPARATOR_TAG.$tag;
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
$item = $this->getItem($key);
if (!$item->isHit()) {
return $default;
}
return $item->get();
}
/**
* {@inheritdoc}
*/
public function set($key, $value, $ttl = null)
{
$item = $this->getItem($key);
$item->set($value);
$item->expiresAfter($ttl);
return $this->save($item);
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
return $this->deleteItem($key);
}
/**
* {@inheritdoc}
*/
public function getMultiple($keys, $default = null)
{
if (!is_array($keys)) {
if (!$keys instanceof \Traversable) {
throw new InvalidArgumentException('$keys is neither an array nor Traversable');
}
// Since we need to throw an exception if *any* key is invalid, it doesn't
// make sense to wrap iterators or something like that.
$keys = iterator_to_array($keys, false);
}
$items = $this->getItems($keys);
return $this->generateValues($default, $items);
}
/**
* @param $default
* @param $items
*
* @return \Generator
*/
private function generateValues($default, $items)
{
foreach ($items as $key => $item) {
/** @type $item CacheItemInterface */
if (!$item->isHit()) {
yield $key => $default;
} else {
yield $key => $item->get();
}
}
}
/**
* {@inheritdoc}
*/
public function setMultiple($values, $ttl = null)
{
if (!is_array($values)) {
if (!$values instanceof \Traversable) {
throw new InvalidArgumentException('$values is neither an array nor Traversable');
}
}
$keys = [];
$arrayValues = [];
foreach ($values as $key => $value) {
if (is_int($key)) {
$key = (string) $key;
}
$this->validateKey($key);
$keys[] = $key;
$arrayValues[$key] = $value;
}
$items = $this->getItems($keys);
$itemSuccess = true;
foreach ($items as $key => $item) {
$item->set($arrayValues[$key]);
try {
$item->expiresAfter($ttl);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$itemSuccess = $itemSuccess && $this->saveDeferred($item);
}
return $itemSuccess && $this->commit();
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($keys)
{
if (!is_array($keys)) {
if (!$keys instanceof \Traversable) {
throw new InvalidArgumentException('$keys is neither an array nor Traversable');
}
// Since we need to throw an exception if *any* key is invalid, it doesn't
// make sense to wrap iterators or something like that.
$keys = iterator_to_array($keys, false);
}
return $this->deleteItems($keys);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return $this->hasItem($key);
}
}