ChainAdapter.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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\Adapter;
  11. use Psr\Cache\CacheItemInterface;
  12. use Psr\Cache\CacheItemPoolInterface;
  13. use Symfony\Component\Cache\CacheItem;
  14. use Symfony\Component\Cache\Exception\BadMethodCallException;
  15. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  16. use Symfony\Component\Cache\PruneableInterface;
  17. use Symfony\Component\Cache\ResettableInterface;
  18. use Symfony\Component\Cache\Traits\ContractsTrait;
  19. use Symfony\Contracts\Cache\CacheInterface;
  20. use Symfony\Contracts\Cache\NamespacedPoolInterface;
  21. use Symfony\Contracts\Service\ResetInterface;
  22. /**
  23. * Chains several adapters together.
  24. *
  25. * Cached items are fetched from the first adapter having them in its data store.
  26. * They are saved and deleted in all adapters at once.
  27. *
  28. * @author Kévin Dunglas <dunglas@gmail.com>
  29. */
  30. class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
  31. {
  32. use ContractsTrait;
  33. private array $adapters = [];
  34. private int $adapterCount;
  35. private static \Closure $syncItem;
  36. /**
  37. * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items
  38. * @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones
  39. */
  40. public function __construct(
  41. array $adapters,
  42. private int $defaultLifetime = 0,
  43. ) {
  44. if (!$adapters) {
  45. throw new InvalidArgumentException('At least one adapter must be specified.');
  46. }
  47. foreach ($adapters as $adapter) {
  48. if (!$adapter instanceof CacheItemPoolInterface) {
  49. throw new InvalidArgumentException(\sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class));
  50. }
  51. if ('cli' === \PHP_SAPI && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
  52. continue; // skip putting APCu in the chain when the backend is disabled
  53. }
  54. if ($adapter instanceof AdapterInterface) {
  55. $this->adapters[] = $adapter;
  56. } else {
  57. $this->adapters[] = new ProxyAdapter($adapter);
  58. }
  59. }
  60. $this->adapterCount = \count($this->adapters);
  61. self::$syncItem ??= \Closure::bind(
  62. static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
  63. $sourceItem->isTaggable = false;
  64. $sourceMetadata ??= $sourceItem->metadata;
  65. $item->value = $sourceItem->value;
  66. $item->isHit = $sourceItem->isHit;
  67. $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;
  68. if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) {
  69. $item->expiresAt(\DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
  70. } elseif (0 < $defaultLifetime) {
  71. $item->expiresAfter($defaultLifetime);
  72. }
  73. return $item;
  74. },
  75. null,
  76. CacheItem::class
  77. );
  78. }
  79. public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
  80. {
  81. $doSave = true;
  82. $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) {
  83. $value = $callback($item, $save);
  84. $doSave = $save;
  85. return $value;
  86. };
  87. $wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$doSave, &$metadata) {
  88. static $lastItem;
  89. static $i = 0;
  90. $adapter = $this->adapters[$i];
  91. if (isset($this->adapters[++$i])) {
  92. $callback = $wrap;
  93. $beta = \INF === $beta ? \INF : 0;
  94. }
  95. if ($adapter instanceof CacheInterface && $i !== $this->adapterCount) {
  96. $value = $adapter->get($key, $callback, $beta, $metadata);
  97. } else {
  98. $value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
  99. }
  100. if (null !== $item) {
  101. (self::$syncItem)($lastItem ??= $item, $item, $this->defaultLifetime, $metadata);
  102. }
  103. $save = $doSave;
  104. return $value;
  105. };
  106. return $wrap();
  107. }
  108. public function getItem(mixed $key): CacheItem
  109. {
  110. $syncItem = self::$syncItem;
  111. $misses = [];
  112. foreach ($this->adapters as $i => $adapter) {
  113. $item = $adapter->getItem($key);
  114. if ($item->isHit()) {
  115. while (0 <= --$i) {
  116. $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime));
  117. }
  118. return $item;
  119. }
  120. $misses[$i] = $item;
  121. }
  122. return $item;
  123. }
  124. public function getItems(array $keys = []): iterable
  125. {
  126. return $this->generateItems($this->adapters[0]->getItems($keys), 0);
  127. }
  128. private function generateItems(iterable $items, int $adapterIndex): \Generator
  129. {
  130. $missing = [];
  131. $misses = [];
  132. $nextAdapterIndex = $adapterIndex + 1;
  133. $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null;
  134. foreach ($items as $k => $item) {
  135. if (!$nextAdapter || $item->isHit()) {
  136. yield $k => $item;
  137. } else {
  138. $missing[] = $k;
  139. $misses[$k] = $item;
  140. }
  141. }
  142. if ($missing) {
  143. $syncItem = self::$syncItem;
  144. $adapter = $this->adapters[$adapterIndex];
  145. $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex);
  146. foreach ($items as $k => $item) {
  147. if ($item->isHit()) {
  148. $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime));
  149. }
  150. yield $k => $item;
  151. }
  152. }
  153. }
  154. public function hasItem(mixed $key): bool
  155. {
  156. foreach ($this->adapters as $adapter) {
  157. if ($adapter->hasItem($key)) {
  158. return true;
  159. }
  160. }
  161. return false;
  162. }
  163. public function clear(string $prefix = ''): bool
  164. {
  165. $cleared = true;
  166. $i = $this->adapterCount;
  167. while ($i--) {
  168. if ($this->adapters[$i] instanceof AdapterInterface) {
  169. $cleared = $this->adapters[$i]->clear($prefix) && $cleared;
  170. } else {
  171. $cleared = $this->adapters[$i]->clear() && $cleared;
  172. }
  173. }
  174. return $cleared;
  175. }
  176. public function deleteItem(mixed $key): bool
  177. {
  178. $deleted = true;
  179. $i = $this->adapterCount;
  180. while ($i--) {
  181. $deleted = $this->adapters[$i]->deleteItem($key) && $deleted;
  182. }
  183. return $deleted;
  184. }
  185. public function deleteItems(array $keys): bool
  186. {
  187. $deleted = true;
  188. $i = $this->adapterCount;
  189. while ($i--) {
  190. $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted;
  191. }
  192. return $deleted;
  193. }
  194. public function save(CacheItemInterface $item): bool
  195. {
  196. $saved = true;
  197. $i = $this->adapterCount;
  198. while ($i--) {
  199. $saved = $this->adapters[$i]->save($item) && $saved;
  200. }
  201. return $saved;
  202. }
  203. public function saveDeferred(CacheItemInterface $item): bool
  204. {
  205. $saved = true;
  206. $i = $this->adapterCount;
  207. while ($i--) {
  208. $saved = $this->adapters[$i]->saveDeferred($item) && $saved;
  209. }
  210. return $saved;
  211. }
  212. public function commit(): bool
  213. {
  214. $committed = true;
  215. $i = $this->adapterCount;
  216. while ($i--) {
  217. $committed = $this->adapters[$i]->commit() && $committed;
  218. }
  219. return $committed;
  220. }
  221. public function prune(): bool
  222. {
  223. $pruned = true;
  224. foreach ($this->adapters as $adapter) {
  225. if ($adapter instanceof PruneableInterface) {
  226. $pruned = $adapter->prune() && $pruned;
  227. }
  228. }
  229. return $pruned;
  230. }
  231. public function withSubNamespace(string $namespace): static
  232. {
  233. $clone = clone $this;
  234. $adapters = [];
  235. foreach ($this->adapters as $adapter) {
  236. if (!$adapter instanceof NamespacedPoolInterface) {
  237. throw new BadMethodCallException('All adapters must implement NamespacedPoolInterface to support namespaces.');
  238. }
  239. $adapters[] = $adapter->withSubNamespace($namespace);
  240. }
  241. $clone->adapters = $adapters;
  242. return $clone;
  243. }
  244. public function reset(): void
  245. {
  246. foreach ($this->adapters as $adapter) {
  247. if ($adapter instanceof ResetInterface) {
  248. $adapter->reset();
  249. }
  250. }
  251. }
  252. }