vendor/symfony/cache/Adapter/PhpArrayAdapter.php line 130

Open in your IDE?
  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\InvalidArgumentException;
  15. use Symfony\Component\Cache\PruneableInterface;
  16. use Symfony\Component\Cache\ResettableInterface;
  17. use Symfony\Component\Cache\Traits\CachedValueInterface;
  18. use Symfony\Component\Cache\Traits\ContractsTrait;
  19. use Symfony\Component\Cache\Traits\ProxyTrait;
  20. use Symfony\Component\VarExporter\VarExporter;
  21. use Symfony\Contracts\Cache\CacheInterface;
  22. /**
  23. * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
  24. * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
  25. *
  26. * @author Titouan Galopin <galopintitouan@gmail.com>
  27. * @author Nicolas Grekas <p@tchwork.com>
  28. */
  29. class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
  30. {
  31. use ContractsTrait;
  32. use ProxyTrait;
  33. private string $file;
  34. private array $keys;
  35. private array $values;
  36. private static \Closure $createCacheItem;
  37. private static array $valuesCache = [];
  38. /**
  39. * @param string $file The PHP file were values are cached
  40. * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
  41. */
  42. public function __construct(string $file, AdapterInterface $fallbackPool)
  43. {
  44. $this->file = $file;
  45. $this->pool = $fallbackPool;
  46. self::$createCacheItem ??= \Closure::bind(
  47. static function ($key, $value, $isHit) {
  48. $item = new CacheItem();
  49. $item->key = $key;
  50. $item->value = $value;
  51. $item->isHit = $isHit;
  52. return $item;
  53. },
  54. null,
  55. CacheItem::class
  56. );
  57. }
  58. /**
  59. * This adapter takes advantage of how PHP stores arrays in its latest versions.
  60. *
  61. * @param string $file The PHP file were values are cached
  62. * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
  63. */
  64. public static function create(string $file, CacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
  65. {
  66. if (!$fallbackPool instanceof AdapterInterface) {
  67. $fallbackPool = new ProxyAdapter($fallbackPool);
  68. }
  69. return new static($file, $fallbackPool);
  70. }
  71. public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
  72. {
  73. if (!isset($this->values)) {
  74. $this->initialize();
  75. }
  76. if (!isset($this->keys[$key])) {
  77. get_from_pool:
  78. if ($this->pool instanceof CacheInterface) {
  79. return $this->pool->get($key, $callback, $beta, $metadata);
  80. }
  81. return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
  82. }
  83. $value = $this->values[$this->keys[$key]];
  84. if ('N;' === $value) {
  85. return null;
  86. }
  87. if (!$value instanceof CachedValueInterface) {
  88. return $value;
  89. }
  90. try {
  91. return $value->getValue();
  92. } catch (\Throwable) {
  93. unset($this->keys[$key]);
  94. goto get_from_pool;
  95. }
  96. }
  97. public function getItem(mixed $key): CacheItem
  98. {
  99. if (!\is_string($key)) {
  100. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  101. }
  102. if (!isset($this->values)) {
  103. $this->initialize();
  104. }
  105. if (!isset($this->keys[$key])) {
  106. return $this->pool->getItem($key);
  107. }
  108. $value = $this->values[$this->keys[$key]];
  109. $isHit = true;
  110. if ('N;' === $value) {
  111. $value = null;
  112. } elseif ($value instanceof CachedValueInterface) {
  113. try {
  114. $value = $value->getValue();
  115. } catch (\Throwable) {
  116. $value = null;
  117. $isHit = false;
  118. }
  119. }
  120. return (self::$createCacheItem)($key, $value, $isHit);
  121. }
  122. public function getItems(array $keys = []): iterable
  123. {
  124. foreach ($keys as $key) {
  125. if (!\is_string($key)) {
  126. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  127. }
  128. }
  129. if (!isset($this->values)) {
  130. $this->initialize();
  131. }
  132. return $this->generateItems($keys);
  133. }
  134. public function hasItem(mixed $key): bool
  135. {
  136. if (!\is_string($key)) {
  137. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  138. }
  139. if (!isset($this->values)) {
  140. $this->initialize();
  141. }
  142. return isset($this->keys[$key]) || $this->pool->hasItem($key);
  143. }
  144. public function deleteItem(mixed $key): bool
  145. {
  146. if (!\is_string($key)) {
  147. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  148. }
  149. if (!isset($this->values)) {
  150. $this->initialize();
  151. }
  152. return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
  153. }
  154. public function deleteItems(array $keys): bool
  155. {
  156. $deleted = true;
  157. $fallbackKeys = [];
  158. foreach ($keys as $key) {
  159. if (!\is_string($key)) {
  160. throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  161. }
  162. if (isset($this->keys[$key])) {
  163. $deleted = false;
  164. } else {
  165. $fallbackKeys[] = $key;
  166. }
  167. }
  168. if (!isset($this->values)) {
  169. $this->initialize();
  170. }
  171. if ($fallbackKeys) {
  172. $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
  173. }
  174. return $deleted;
  175. }
  176. public function save(CacheItemInterface $item): bool
  177. {
  178. if (!isset($this->values)) {
  179. $this->initialize();
  180. }
  181. return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
  182. }
  183. public function saveDeferred(CacheItemInterface $item): bool
  184. {
  185. if (!isset($this->values)) {
  186. $this->initialize();
  187. }
  188. return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
  189. }
  190. public function commit(): bool
  191. {
  192. return $this->pool->commit();
  193. }
  194. public function clear(string $prefix = ''): bool
  195. {
  196. $this->keys = $this->values = [];
  197. $cleared = @unlink($this->file) || !file_exists($this->file);
  198. unset(self::$valuesCache[$this->file]);
  199. if ($this->pool instanceof AdapterInterface) {
  200. return $this->pool->clear($prefix) && $cleared;
  201. }
  202. return $this->pool->clear() && $cleared;
  203. }
  204. /**
  205. * Store an array of cached values.
  206. *
  207. * @param array $values The cached values
  208. *
  209. * @return string[] A list of classes to preload on PHP 7.4+
  210. */
  211. public function warmUp(array $values): array
  212. {
  213. if (file_exists($this->file)) {
  214. if (!is_file($this->file)) {
  215. throw new InvalidArgumentException(\sprintf('Cache path exists and is not a file: "%s".', $this->file));
  216. }
  217. if (!is_writable($this->file)) {
  218. throw new InvalidArgumentException(\sprintf('Cache file is not writable: "%s".', $this->file));
  219. }
  220. } else {
  221. $directory = \dirname($this->file);
  222. if (!is_dir($directory) && !@mkdir($directory, 0o777, true)) {
  223. throw new InvalidArgumentException(\sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
  224. }
  225. if (!is_writable($directory)) {
  226. throw new InvalidArgumentException(\sprintf('Cache directory is not writable: "%s".', $directory));
  227. }
  228. }
  229. $preload = [];
  230. $dumpedValues = '';
  231. $dumpedMap = [];
  232. $dump = <<<'EOF'
  233. <?php
  234. // This file has been auto-generated by the Symfony Cache Component.
  235. return [[
  236. EOF;
  237. foreach ($values as $key => $value) {
  238. CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
  239. $isStaticValue = true;
  240. if (null === $value) {
  241. $value = "'N;'";
  242. } elseif (\is_object($value) || \is_array($value)) {
  243. try {
  244. $value = VarExporter::export($value, $isStaticValue, $preload);
  245. } catch (\Exception $e) {
  246. throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
  247. }
  248. } elseif (\is_string($value)) {
  249. // Wrap "N;" in a closure to not confuse it with an encoded `null`
  250. if ('N;' === $value) {
  251. $isStaticValue = false;
  252. }
  253. $value = var_export($value, true);
  254. } elseif (!\is_scalar($value)) {
  255. throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
  256. } else {
  257. $value = var_export($value, true);
  258. }
  259. if (!$isStaticValue) {
  260. $value = 'new class() implements \\'.CachedValueInterface::class." { public function getValue(): mixed { return {$value}; } }";
  261. }
  262. $hash = hash('xxh128', $value);
  263. if (null === $id = $dumpedMap[$hash] ?? null) {
  264. $id = $dumpedMap[$hash] = \count($dumpedMap);
  265. $dumpedValues .= "{$id} => {$value},\n";
  266. }
  267. $dump .= var_export($key, true)." => {$id},\n";
  268. }
  269. $dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
  270. $tmpFile = uniqid($this->file, true);
  271. file_put_contents($tmpFile, $dump);
  272. @chmod($tmpFile, 0o666 & ~umask());
  273. unset($serialized, $value, $dump);
  274. @rename($tmpFile, $this->file);
  275. unset(self::$valuesCache[$this->file]);
  276. $this->initialize();
  277. return $preload;
  278. }
  279. /**
  280. * Load the cache file.
  281. */
  282. private function initialize(): void
  283. {
  284. if (isset(self::$valuesCache[$this->file])) {
  285. $values = self::$valuesCache[$this->file];
  286. } elseif (!is_file($this->file)) {
  287. $this->keys = $this->values = [];
  288. return;
  289. } else {
  290. $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
  291. }
  292. if (2 !== \count($values) || !isset($values[0], $values[1])) {
  293. $this->keys = $this->values = [];
  294. } else {
  295. [$this->keys, $this->values] = $values;
  296. }
  297. }
  298. private function generateItems(array $keys): \Generator
  299. {
  300. $f = self::$createCacheItem;
  301. $fallbackKeys = [];
  302. foreach ($keys as $key) {
  303. if (isset($this->keys[$key])) {
  304. $value = $this->values[$this->keys[$key]];
  305. if ('N;' === $value) {
  306. yield $key => $f($key, null, true);
  307. } elseif ($value instanceof CachedValueInterface) {
  308. try {
  309. yield $key => $f($key, $value->getValue(), true);
  310. } catch (\Throwable) {
  311. yield $key => $f($key, null, false);
  312. }
  313. } else {
  314. yield $key => $f($key, $value, true);
  315. }
  316. } else {
  317. $fallbackKeys[] = $key;
  318. }
  319. }
  320. if ($fallbackKeys) {
  321. yield from $this->pool->getItems($fallbackKeys);
  322. }
  323. }
  324. }