vendor/symfony/cache/Adapter/PhpFilesAdapter.php line 96

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 Symfony\Component\Cache\Exception\CacheException;
  12. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  13. use Symfony\Component\Cache\PruneableInterface;
  14. use Symfony\Component\Cache\Traits\CachedValueInterface;
  15. use Symfony\Component\Cache\Traits\FilesystemCommonTrait;
  16. use Symfony\Component\VarExporter\VarExporter;
  17. /**
  18. * @author Piotr Stankowski <git@trakos.pl>
  19. * @author Nicolas Grekas <p@tchwork.com>
  20. * @author Rob Frawley 2nd <rmf@src.run>
  21. */
  22. class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
  23. {
  24. use FilesystemCommonTrait {
  25. doClear as private doCommonClear;
  26. doDelete as private doCommonDelete;
  27. }
  28. private \Closure $includeHandler;
  29. private bool $appendOnly;
  30. private array $values = [];
  31. private array $files = [];
  32. private static int $startTime;
  33. private static array $valuesCache = [];
  34. /**
  35. * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
  36. * Doing so is encouraged because it fits perfectly OPcache's memory model.
  37. *
  38. * @throws CacheException if OPcache is not enabled
  39. */
  40. public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, bool $appendOnly = false)
  41. {
  42. $this->appendOnly = $appendOnly;
  43. self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
  44. parent::__construct('', $defaultLifetime);
  45. $this->init($namespace, $directory);
  46. $this->includeHandler = static function ($type, $msg, $file, $line) {
  47. throw new \ErrorException($msg, 0, $type, $file, $line);
  48. };
  49. }
  50. /**
  51. * @return bool
  52. */
  53. public static function isSupported()
  54. {
  55. self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
  56. return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL));
  57. }
  58. public function prune(): bool
  59. {
  60. $time = time();
  61. $pruned = true;
  62. $getExpiry = true;
  63. set_error_handler($this->includeHandler);
  64. try {
  65. foreach ($this->scanHashDir($this->directory) as $file) {
  66. try {
  67. if (\is_array($expiresAt = include $file)) {
  68. $expiresAt = $expiresAt[0];
  69. }
  70. } catch (\ErrorException $e) {
  71. $expiresAt = $time;
  72. }
  73. if ($time >= $expiresAt) {
  74. $pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
  75. }
  76. }
  77. } finally {
  78. restore_error_handler();
  79. }
  80. return $pruned;
  81. }
  82. protected function doFetch(array $ids): iterable
  83. {
  84. if ($this->appendOnly) {
  85. $now = 0;
  86. $missingIds = [];
  87. } else {
  88. $now = time();
  89. $missingIds = $ids;
  90. $ids = [];
  91. }
  92. $values = [];
  93. begin:
  94. $getExpiry = false;
  95. foreach ($ids as $id) {
  96. if (null === $value = $this->values[$id] ?? null) {
  97. $missingIds[] = $id;
  98. } elseif ('N;' === $value) {
  99. $values[$id] = null;
  100. } elseif (!\is_object($value)) {
  101. $values[$id] = $value;
  102. } elseif ($value instanceof CachedValueInterface) {
  103. $values[$id] = $value->getValue();
  104. } elseif (!$value instanceof LazyValue) {
  105. $values[$id] = $value;
  106. } elseif (false === $values[$id] = include $value->file) {
  107. unset($values[$id], $this->values[$id]);
  108. $missingIds[] = $id;
  109. }
  110. if (!$this->appendOnly) {
  111. unset($this->values[$id]);
  112. }
  113. }
  114. if (!$missingIds) {
  115. return $values;
  116. }
  117. set_error_handler($this->includeHandler);
  118. try {
  119. $getExpiry = true;
  120. foreach ($missingIds as $k => $id) {
  121. try {
  122. $file = $this->files[$id] ??= $this->getFile($id);
  123. if (isset(self::$valuesCache[$file])) {
  124. [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
  125. } elseif (\is_array($expiresAt = include $file)) {
  126. if ($this->appendOnly) {
  127. self::$valuesCache[$file] = $expiresAt;
  128. }
  129. [$expiresAt, $this->values[$id]] = $expiresAt;
  130. } elseif ($now < $expiresAt) {
  131. $this->values[$id] = new LazyValue($file);
  132. }
  133. if ($now >= $expiresAt) {
  134. unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
  135. }
  136. } catch (\ErrorException) {
  137. unset($missingIds[$k]);
  138. }
  139. }
  140. } finally {
  141. restore_error_handler();
  142. }
  143. $ids = $missingIds;
  144. $missingIds = [];
  145. goto begin;
  146. }
  147. protected function doHave(string $id): bool
  148. {
  149. if ($this->appendOnly && isset($this->values[$id])) {
  150. return true;
  151. }
  152. set_error_handler($this->includeHandler);
  153. try {
  154. $file = $this->files[$id] ??= $this->getFile($id);
  155. $getExpiry = true;
  156. if (isset(self::$valuesCache[$file])) {
  157. [$expiresAt, $value] = self::$valuesCache[$file];
  158. } elseif (\is_array($expiresAt = include $file)) {
  159. if ($this->appendOnly) {
  160. self::$valuesCache[$file] = $expiresAt;
  161. }
  162. [$expiresAt, $value] = $expiresAt;
  163. } elseif ($this->appendOnly) {
  164. $value = new LazyValue($file);
  165. }
  166. } catch (\ErrorException) {
  167. return false;
  168. } finally {
  169. restore_error_handler();
  170. }
  171. if ($this->appendOnly) {
  172. $now = 0;
  173. $this->values[$id] = $value;
  174. } else {
  175. $now = time();
  176. }
  177. return $now < $expiresAt;
  178. }
  179. protected function doSave(array $values, int $lifetime): array|bool
  180. {
  181. $ok = true;
  182. $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
  183. $allowCompile = self::isSupported();
  184. foreach ($values as $key => $value) {
  185. unset($this->values[$key]);
  186. $isStaticValue = true;
  187. if (null === $value) {
  188. $value = "'N;'";
  189. } elseif (\is_object($value) || \is_array($value)) {
  190. try {
  191. $value = VarExporter::export($value, $isStaticValue);
  192. } catch (\Exception $e) {
  193. throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
  194. }
  195. } elseif (\is_string($value)) {
  196. // Wrap "N;" in a closure to not confuse it with an encoded `null`
  197. if ('N;' === $value) {
  198. $isStaticValue = false;
  199. }
  200. $value = var_export($value, true);
  201. } elseif (!\is_scalar($value)) {
  202. throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
  203. } else {
  204. $value = var_export($value, true);
  205. }
  206. $encodedKey = rawurlencode($key);
  207. if ($isStaticValue) {
  208. $value = "return [{$expiry}, {$value}];";
  209. } elseif ($this->appendOnly) {
  210. $value = "return [{$expiry}, new class() implements \\".CachedValueInterface::class." { public function getValue(): mixed { return {$value}; } }];";
  211. } else {
  212. // We cannot use a closure here because of https://bugs.php.net/76982
  213. $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
  214. $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
  215. }
  216. $file = $this->files[$key] = $this->getFile($key, true);
  217. // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
  218. $ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;
  219. if ($allowCompile) {
  220. @opcache_invalidate($file, true);
  221. @opcache_compile_file($file);
  222. }
  223. unset(self::$valuesCache[$file]);
  224. }
  225. if (!$ok && !is_writable($this->directory)) {
  226. throw new CacheException(\sprintf('Cache directory is not writable (%s).', $this->directory));
  227. }
  228. return $ok;
  229. }
  230. protected function doClear(string $namespace): bool
  231. {
  232. $this->values = [];
  233. return $this->doCommonClear($namespace);
  234. }
  235. protected function doDelete(array $ids): bool
  236. {
  237. foreach ($ids as $id) {
  238. unset($this->values[$id]);
  239. }
  240. return $this->doCommonDelete($ids);
  241. }
  242. /**
  243. * @return bool
  244. */
  245. protected function doUnlink(string $file)
  246. {
  247. unset(self::$valuesCache[$file]);
  248. if (self::isSupported()) {
  249. @opcache_invalidate($file, true);
  250. }
  251. return @unlink($file);
  252. }
  253. private function getFileKey(string $file): string
  254. {
  255. if (!$h = @fopen($file, 'r')) {
  256. return '';
  257. }
  258. $encodedKey = substr(fgets($h), 8);
  259. fclose($h);
  260. return rawurldecode(rtrim($encodedKey));
  261. }
  262. }
  263. /**
  264. * @internal
  265. */
  266. class LazyValue
  267. {
  268. public string $file;
  269. public function __construct(string $file)
  270. {
  271. $this->file = $file;
  272. }
  273. }