PhpFilesAdapter.php 10 KB

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