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

2 years ago
  1. <?php
  2. /*
  3. * This file is part of php-cache organization.
  4. *
  5. * (c) 2015 Aaron Scherer <aequasi@gmail.com>, Tobias Nyholm <tobias.nyholm@gmail.com>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Cache\Adapter\Common;
  11. use Cache\Adapter\Common\Exception\CacheException;
  12. use Cache\Adapter\Common\Exception\CachePoolException;
  13. use Cache\Adapter\Common\Exception\InvalidArgumentException;
  14. use Psr\Cache\CacheItemInterface;
  15. use Psr\Log\LoggerAwareInterface;
  16. use Psr\Log\LoggerInterface;
  17. use Psr\SimpleCache\CacheInterface;
  18. /**
  19. * @author Aaron Scherer <aequasi@gmail.com>
  20. * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  21. */
  22. abstract class AbstractCachePool implements PhpCachePool, LoggerAwareInterface, CacheInterface
  23. {
  24. const SEPARATOR_TAG = '!';
  25. /**
  26. * @type LoggerInterface
  27. */
  28. private $logger;
  29. /**
  30. * @type PhpCacheItem[] deferred
  31. */
  32. protected $deferred = [];
  33. /**
  34. * @param PhpCacheItem $item
  35. * @param int|null $ttl seconds from now
  36. *
  37. * @return bool true if saved
  38. */
  39. abstract protected function storeItemInCache(PhpCacheItem $item, $ttl);
  40. /**
  41. * Fetch an object from the cache implementation.
  42. *
  43. * If it is a cache miss, it MUST return [false, null, [], null]
  44. *
  45. * @param string $key
  46. *
  47. * @return array with [isHit, value, tags[], expirationTimestamp]
  48. */
  49. abstract protected function fetchObjectFromCache($key);
  50. /**
  51. * Clear all objects from cache.
  52. *
  53. * @return bool false if error
  54. */
  55. abstract protected function clearAllObjectsFromCache();
  56. /**
  57. * Remove one object from cache.
  58. *
  59. * @param string $key
  60. *
  61. * @return bool
  62. */
  63. abstract protected function clearOneObjectFromCache($key);
  64. /**
  65. * Get an array with all the values in the list named $name.
  66. *
  67. * @param string $name
  68. *
  69. * @return array
  70. */
  71. abstract protected function getList($name);
  72. /**
  73. * Remove the list.
  74. *
  75. * @param string $name
  76. *
  77. * @return bool
  78. */
  79. abstract protected function removeList($name);
  80. /**
  81. * Add a item key on a list named $name.
  82. *
  83. * @param string $name
  84. * @param string $key
  85. */
  86. abstract protected function appendListItem($name, $key);
  87. /**
  88. * Remove an item from the list.
  89. *
  90. * @param string $name
  91. * @param string $key
  92. */
  93. abstract protected function removeListItem($name, $key);
  94. /**
  95. * Make sure to commit before we destruct.
  96. */
  97. public function __destruct()
  98. {
  99. $this->commit();
  100. }
  101. /**
  102. * {@inheritdoc}
  103. */
  104. public function getItem($key)
  105. {
  106. $this->validateKey($key);
  107. if (isset($this->deferred[$key])) {
  108. /** @type CacheItem $item */
  109. $item = clone $this->deferred[$key];
  110. $item->moveTagsToPrevious();
  111. return $item;
  112. }
  113. $func = function () use ($key) {
  114. try {
  115. return $this->fetchObjectFromCache($key);
  116. } catch (\Exception $e) {
  117. $this->handleException($e, __FUNCTION__);
  118. }
  119. };
  120. return new CacheItem($key, $func);
  121. }
  122. /**
  123. * {@inheritdoc}
  124. */
  125. public function getItems(array $keys = [])
  126. {
  127. $items = [];
  128. foreach ($keys as $key) {
  129. $items[$key] = $this->getItem($key);
  130. }
  131. return $items;
  132. }
  133. /**
  134. * {@inheritdoc}
  135. */
  136. public function hasItem($key)
  137. {
  138. try {
  139. return $this->getItem($key)->isHit();
  140. } catch (\Exception $e) {
  141. $this->handleException($e, __FUNCTION__);
  142. }
  143. }
  144. /**
  145. * {@inheritdoc}
  146. */
  147. public function clear()
  148. {
  149. // Clear the deferred items
  150. $this->deferred = [];
  151. try {
  152. return $this->clearAllObjectsFromCache();
  153. } catch (\Exception $e) {
  154. $this->handleException($e, __FUNCTION__);
  155. }
  156. }
  157. /**
  158. * {@inheritdoc}
  159. */
  160. public function deleteItem($key)
  161. {
  162. try {
  163. return $this->deleteItems([$key]);
  164. } catch (\Exception $e) {
  165. $this->handleException($e, __FUNCTION__);
  166. }
  167. }
  168. /**
  169. * {@inheritdoc}
  170. */
  171. public function deleteItems(array $keys)
  172. {
  173. $deleted = true;
  174. foreach ($keys as $key) {
  175. $this->validateKey($key);
  176. // Delete form deferred
  177. unset($this->deferred[$key]);
  178. // We have to commit here to be able to remove deferred hierarchy items
  179. $this->commit();
  180. $this->preRemoveItem($key);
  181. if (!$this->clearOneObjectFromCache($key)) {
  182. $deleted = false;
  183. }
  184. }
  185. return $deleted;
  186. }
  187. /**
  188. * {@inheritdoc}
  189. */
  190. public function save(CacheItemInterface $item)
  191. {
  192. if (!$item instanceof PhpCacheItem) {
  193. $e = new InvalidArgumentException('Cache items are not transferable between pools. Item MUST implement PhpCacheItem.');
  194. $this->handleException($e, __FUNCTION__);
  195. }
  196. $this->removeTagEntries($item);
  197. $this->saveTags($item);
  198. $timeToLive = null;
  199. if (null !== $timestamp = $item->getExpirationTimestamp()) {
  200. $timeToLive = $timestamp - time();
  201. if ($timeToLive < 0) {
  202. return $this->deleteItem($item->getKey());
  203. }
  204. }
  205. try {
  206. return $this->storeItemInCache($item, $timeToLive);
  207. } catch (\Exception $e) {
  208. $this->handleException($e, __FUNCTION__);
  209. }
  210. }
  211. /**
  212. * {@inheritdoc}
  213. */
  214. public function saveDeferred(CacheItemInterface $item)
  215. {
  216. $this->deferred[$item->getKey()] = $item;
  217. return true;
  218. }
  219. /**
  220. * {@inheritdoc}
  221. */
  222. public function commit()
  223. {
  224. $saved = true;
  225. foreach ($this->deferred as $item) {
  226. if (!$this->save($item)) {
  227. $saved = false;
  228. }
  229. }
  230. $this->deferred = [];
  231. return $saved;
  232. }
  233. /**
  234. * @param string $key
  235. *
  236. * @throws InvalidArgumentException
  237. */
  238. protected function validateKey($key)
  239. {
  240. if (!is_string($key)) {
  241. $e = new InvalidArgumentException(sprintf(
  242. 'Cache key must be string, "%s" given',
  243. gettype($key)
  244. ));
  245. $this->handleException($e, __FUNCTION__);
  246. }
  247. if (!isset($key[0])) {
  248. $e = new InvalidArgumentException('Cache key cannot be an empty string');
  249. $this->handleException($e, __FUNCTION__);
  250. }
  251. if (preg_match('|[\{\}\(\)/\\\@\:]|', $key)) {
  252. $e = new InvalidArgumentException(sprintf(
  253. 'Invalid key: "%s". The key contains one or more characters reserved for future extension: {}()/\@:',
  254. $key
  255. ));
  256. $this->handleException($e, __FUNCTION__);
  257. }
  258. }
  259. /**
  260. * @param LoggerInterface $logger
  261. */
  262. public function setLogger(LoggerInterface $logger): void
  263. {
  264. $this->logger = $logger;
  265. }
  266. /**
  267. * Logs with an arbitrary level if the logger exists.
  268. *
  269. * @param mixed $level
  270. * @param string $message
  271. * @param array $context
  272. */
  273. protected function log($level, $message, array $context = [])
  274. {
  275. if ($this->logger !== null) {
  276. $this->logger->log($level, $message, $context);
  277. }
  278. }
  279. /**
  280. * Log exception and rethrow it.
  281. *
  282. * @param \Exception $e
  283. * @param string $function
  284. *
  285. * @throws CachePoolException
  286. */
  287. private function handleException(\Exception $e, $function)
  288. {
  289. $level = 'alert';
  290. if ($e instanceof InvalidArgumentException) {
  291. $level = 'warning';
  292. }
  293. $this->log($level, $e->getMessage(), ['exception' => $e]);
  294. if (!$e instanceof CacheException) {
  295. $e = new CachePoolException(sprintf('Exception thrown when executing "%s". ', $function), 0, $e);
  296. }
  297. throw $e;
  298. }
  299. /**
  300. * @param array $tags
  301. *
  302. * @return bool
  303. */
  304. public function invalidateTags(array $tags)
  305. {
  306. $itemIds = [];
  307. foreach ($tags as $tag) {
  308. $itemIds = array_merge($itemIds, $this->getList($this->getTagKey($tag)));
  309. }
  310. // Remove all items with the tag
  311. $success = $this->deleteItems($itemIds);
  312. if ($success) {
  313. // Remove the tag list
  314. foreach ($tags as $tag) {
  315. $this->removeList($this->getTagKey($tag));
  316. $l = $this->getList($this->getTagKey($tag));
  317. }
  318. }
  319. return $success;
  320. }
  321. public function invalidateTag($tag)
  322. {
  323. return $this->invalidateTags([$tag]);
  324. }
  325. /**
  326. * @param PhpCacheItem $item
  327. */
  328. protected function saveTags(PhpCacheItem $item)
  329. {
  330. $tags = $item->getTags();
  331. foreach ($tags as $tag) {
  332. $this->appendListItem($this->getTagKey($tag), $item->getKey());
  333. }
  334. }
  335. /**
  336. * Removes the key form all tag lists. When an item with tags is removed
  337. * we MUST remove the tags. If we fail to remove the tags a new item with
  338. * the same key will automatically get the previous tags.
  339. *
  340. * @param string $key
  341. *
  342. * @return $this
  343. */
  344. protected function preRemoveItem($key)
  345. {
  346. $item = $this->getItem($key);
  347. $this->removeTagEntries($item);
  348. return $this;
  349. }
  350. /**
  351. * @param PhpCacheItem $item
  352. */
  353. private function removeTagEntries(PhpCacheItem $item)
  354. {
  355. $tags = $item->getPreviousTags();
  356. foreach ($tags as $tag) {
  357. $this->removeListItem($this->getTagKey($tag), $item->getKey());
  358. }
  359. }
  360. /**
  361. * @param string $tag
  362. *
  363. * @return string
  364. */
  365. protected function getTagKey($tag)
  366. {
  367. return 'tag'.self::SEPARATOR_TAG.$tag;
  368. }
  369. /**
  370. * {@inheritdoc}
  371. */
  372. public function get($key, $default = null)
  373. {
  374. $item = $this->getItem($key);
  375. if (!$item->isHit()) {
  376. return $default;
  377. }
  378. return $item->get();
  379. }
  380. /**
  381. * {@inheritdoc}
  382. */
  383. public function set($key, $value, $ttl = null)
  384. {
  385. $item = $this->getItem($key);
  386. $item->set($value);
  387. $item->expiresAfter($ttl);
  388. return $this->save($item);
  389. }
  390. /**
  391. * {@inheritdoc}
  392. */
  393. public function delete($key)
  394. {
  395. return $this->deleteItem($key);
  396. }
  397. /**
  398. * {@inheritdoc}
  399. */
  400. public function getMultiple($keys, $default = null)
  401. {
  402. if (!is_array($keys)) {
  403. if (!$keys instanceof \Traversable) {
  404. throw new InvalidArgumentException('$keys is neither an array nor Traversable');
  405. }
  406. // Since we need to throw an exception if *any* key is invalid, it doesn't
  407. // make sense to wrap iterators or something like that.
  408. $keys = iterator_to_array($keys, false);
  409. }
  410. $items = $this->getItems($keys);
  411. return $this->generateValues($default, $items);
  412. }
  413. /**
  414. * @param $default
  415. * @param $items
  416. *
  417. * @return \Generator
  418. */
  419. private function generateValues($default, $items)
  420. {
  421. foreach ($items as $key => $item) {
  422. /** @type $item CacheItemInterface */
  423. if (!$item->isHit()) {
  424. yield $key => $default;
  425. } else {
  426. yield $key => $item->get();
  427. }
  428. }
  429. }
  430. /**
  431. * {@inheritdoc}
  432. */
  433. public function setMultiple($values, $ttl = null)
  434. {
  435. if (!is_array($values)) {
  436. if (!$values instanceof \Traversable) {
  437. throw new InvalidArgumentException('$values is neither an array nor Traversable');
  438. }
  439. }
  440. $keys = [];
  441. $arrayValues = [];
  442. foreach ($values as $key => $value) {
  443. if (is_int($key)) {
  444. $key = (string) $key;
  445. }
  446. $this->validateKey($key);
  447. $keys[] = $key;
  448. $arrayValues[$key] = $value;
  449. }
  450. $items = $this->getItems($keys);
  451. $itemSuccess = true;
  452. foreach ($items as $key => $item) {
  453. $item->set($arrayValues[$key]);
  454. try {
  455. $item->expiresAfter($ttl);
  456. } catch (InvalidArgumentException $e) {
  457. throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
  458. }
  459. $itemSuccess = $itemSuccess && $this->saveDeferred($item);
  460. }
  461. return $itemSuccess && $this->commit();
  462. }
  463. /**
  464. * {@inheritdoc}
  465. */
  466. public function deleteMultiple($keys)
  467. {
  468. if (!is_array($keys)) {
  469. if (!$keys instanceof \Traversable) {
  470. throw new InvalidArgumentException('$keys is neither an array nor Traversable');
  471. }
  472. // Since we need to throw an exception if *any* key is invalid, it doesn't
  473. // make sense to wrap iterators or something like that.
  474. $keys = iterator_to_array($keys, false);
  475. }
  476. return $this->deleteItems($keys);
  477. }
  478. /**
  479. * {@inheritdoc}
  480. */
  481. public function has($key)
  482. {
  483. return $this->hasItem($key);
  484. }
  485. }