PhpFilesAdapter.php 9.8 KB

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