AbstractTrait.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Cache\Traits;
  11. use Psr\Log\LoggerAwareTrait;
  12. use Symfony\Component\Cache\CacheItem;
  13. /**
  14. * @author Nicolas Grekas <p@tchwork.com>
  15. *
  16. * @internal
  17. */
  18. trait AbstractTrait
  19. {
  20. use LoggerAwareTrait;
  21. private $namespace;
  22. private $namespaceVersion = '';
  23. private $versioningIsEnabled = false;
  24. private $deferred = [];
  25. /**
  26. * @var int|null The maximum length to enforce for identifiers or null when no limit applies
  27. */
  28. protected $maxIdLength;
  29. /**
  30. * Fetches several cache items.
  31. *
  32. * @param array $ids The cache identifiers to fetch
  33. *
  34. * @return array|\Traversable The corresponding values found in the cache
  35. */
  36. abstract protected function doFetch(array $ids);
  37. /**
  38. * Confirms if the cache contains specified cache item.
  39. *
  40. * @param string $id The identifier for which to check existence
  41. *
  42. * @return bool True if item exists in the cache, false otherwise
  43. */
  44. abstract protected function doHave($id);
  45. /**
  46. * Deletes all items in the pool.
  47. *
  48. * @param string $namespace The prefix used for all identifiers managed by this pool
  49. *
  50. * @return bool True if the pool was successfully cleared, false otherwise
  51. */
  52. abstract protected function doClear($namespace);
  53. /**
  54. * Removes multiple items from the pool.
  55. *
  56. * @param array $ids An array of identifiers that should be removed from the pool
  57. *
  58. * @return bool True if the items were successfully removed, false otherwise
  59. */
  60. abstract protected function doDelete(array $ids);
  61. /**
  62. * Persists several cache items immediately.
  63. *
  64. * @param array $values The values to cache, indexed by their cache identifier
  65. * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
  66. *
  67. * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not
  68. */
  69. abstract protected function doSave(array $values, $lifetime);
  70. /**
  71. * {@inheritdoc}
  72. */
  73. public function hasItem($key)
  74. {
  75. $id = $this->getId($key);
  76. if (isset($this->deferred[$key])) {
  77. $this->commit();
  78. }
  79. try {
  80. return $this->doHave($id);
  81. } catch (\Exception $e) {
  82. CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached', ['key' => $key, 'exception' => $e]);
  83. return false;
  84. }
  85. }
  86. /**
  87. * {@inheritdoc}
  88. */
  89. public function clear()
  90. {
  91. $this->deferred = [];
  92. if ($cleared = $this->versioningIsEnabled) {
  93. $namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), ':', 5);
  94. try {
  95. $cleared = $this->doSave(['@'.$this->namespace => $namespaceVersion], 0);
  96. } catch (\Exception $e) {
  97. $cleared = false;
  98. }
  99. if ($cleared = true === $cleared || [] === $cleared) {
  100. $this->namespaceVersion = $namespaceVersion;
  101. }
  102. }
  103. try {
  104. return $this->doClear($this->namespace) || $cleared;
  105. } catch (\Exception $e) {
  106. CacheItem::log($this->logger, 'Failed to clear the cache', ['exception' => $e]);
  107. return false;
  108. }
  109. }
  110. /**
  111. * {@inheritdoc}
  112. */
  113. public function deleteItem($key)
  114. {
  115. return $this->deleteItems([$key]);
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function deleteItems(array $keys)
  121. {
  122. $ids = [];
  123. foreach ($keys as $key) {
  124. $ids[$key] = $this->getId($key);
  125. unset($this->deferred[$key]);
  126. }
  127. try {
  128. if ($this->doDelete($ids)) {
  129. return true;
  130. }
  131. } catch (\Exception $e) {
  132. }
  133. $ok = true;
  134. // When bulk-delete failed, retry each item individually
  135. foreach ($ids as $key => $id) {
  136. try {
  137. $e = null;
  138. if ($this->doDelete([$id])) {
  139. continue;
  140. }
  141. } catch (\Exception $e) {
  142. }
  143. CacheItem::log($this->logger, 'Failed to delete key "{key}"', ['key' => $key, 'exception' => $e]);
  144. $ok = false;
  145. }
  146. return $ok;
  147. }
  148. /**
  149. * Enables/disables versioning of items.
  150. *
  151. * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed,
  152. * but old keys may need garbage collection and extra round-trips to the back-end are required.
  153. *
  154. * Calling this method also clears the memoized namespace version and thus forces a resynchonization of it.
  155. *
  156. * @param bool $enable
  157. *
  158. * @return bool the previous state of versioning
  159. */
  160. public function enableVersioning($enable = true)
  161. {
  162. $wasEnabled = $this->versioningIsEnabled;
  163. $this->versioningIsEnabled = (bool) $enable;
  164. $this->namespaceVersion = '';
  165. return $wasEnabled;
  166. }
  167. /**
  168. * {@inheritdoc}
  169. */
  170. public function reset()
  171. {
  172. if ($this->deferred) {
  173. $this->commit();
  174. }
  175. $this->namespaceVersion = '';
  176. }
  177. /**
  178. * Like the native unserialize() function but throws an exception if anything goes wrong.
  179. *
  180. * @param string $value
  181. *
  182. * @return mixed
  183. *
  184. * @throws \Exception
  185. */
  186. protected static function unserialize($value)
  187. {
  188. if ('b:0;' === $value) {
  189. return false;
  190. }
  191. $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
  192. try {
  193. if (false !== $value = unserialize($value)) {
  194. return $value;
  195. }
  196. throw new \DomainException('Failed to unserialize cached value');
  197. } catch (\Error $e) {
  198. throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
  199. } finally {
  200. ini_set('unserialize_callback_func', $unserializeCallbackHandler);
  201. }
  202. }
  203. private function getId($key)
  204. {
  205. CacheItem::validateKey($key);
  206. if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
  207. $this->namespaceVersion = '1:';
  208. try {
  209. foreach ($this->doFetch(['@'.$this->namespace]) as $v) {
  210. $this->namespaceVersion = $v;
  211. }
  212. if ('1:' === $this->namespaceVersion) {
  213. $this->namespaceVersion = substr_replace(base64_encode(pack('V', time())), ':', 5);
  214. $this->doSave(['@'.$this->namespace => $this->namespaceVersion], 0);
  215. }
  216. } catch (\Exception $e) {
  217. }
  218. }
  219. if (null === $this->maxIdLength) {
  220. return $this->namespace.$this->namespaceVersion.$key;
  221. }
  222. if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
  223. $id = $this->namespace.$this->namespaceVersion.substr_replace(base64_encode(hash('sha256', $key, true)), ':', -(\strlen($this->namespaceVersion) + 22));
  224. }
  225. return $id;
  226. }
  227. /**
  228. * @internal
  229. */
  230. public static function handleUnserializeCallback($class)
  231. {
  232. throw new \DomainException('Class not found: '.$class);
  233. }
  234. }