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
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);
|
|
}
|
|
}
|