PhpFilesAdapter.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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 $includeHandler;
  28. private $appendOnly;
  29. private $values = [];
  30. private $files = [];
  31. private static $startTime;
  32. private static $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. /**
  55. * @return bool
  56. */
  57. public function prune()
  58. {
  59. $time = time();
  60. $pruned = true;
  61. $getExpiry = true;
  62. set_error_handler($this->includeHandler);
  63. try {
  64. foreach ($this->scanHashDir($this->directory) as $file) {
  65. try {
  66. if (\is_array($expiresAt = include $file)) {
  67. $expiresAt = $expiresAt[0];
  68. }
  69. } catch (\ErrorException $e) {
  70. $expiresAt = $time;
  71. }
  72. if ($time >= $expiresAt) {
  73. $pruned = $this->doUnlink($file) && !file_exists($file) && $pruned;
  74. }
  75. }
  76. } finally {
  77. restore_error_handler();
  78. }
  79. return $pruned;
  80. }
  81. /**
  82. * {@inheritdoc}
  83. */
  84. protected function doFetch(array $ids)
  85. {
  86. if ($this->appendOnly) {
  87. $now = 0;
  88. $missingIds = [];
  89. } else {
  90. $now = time();
  91. $missingIds = $ids;
  92. $ids = [];
  93. }
  94. $values = [];
  95. begin:
  96. $getExpiry = false;
  97. foreach ($ids as $id) {
  98. if (null === $value = $this->values[$id] ?? null) {
  99. $missingIds[] = $id;
  100. } elseif ('N;' === $value) {
  101. $values[$id] = null;
  102. } elseif (!\is_object($value)) {
  103. $values[$id] = $value;
  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->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 $e) {
  137. unset($missingIds[$k]);
  138. }
  139. }
  140. } finally {
  141. restore_error_handler();
  142. }
  143. $ids = $missingIds;
  144. $missingIds = [];
  145. goto begin;
  146. }
  147. /**
  148. * {@inheritdoc}
  149. */
  150. protected function doHave(string $id)
  151. {
  152. if ($this->appendOnly && isset($this->values[$id])) {
  153. return true;
  154. }
  155. set_error_handler($this->includeHandler);
  156. try {
  157. $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
  158. $getExpiry = true;
  159. if (isset(self::$valuesCache[$file])) {
  160. [$expiresAt, $value] = self::$valuesCache[$file];
  161. } elseif (\is_array($expiresAt = include $file)) {
  162. if ($this->appendOnly) {
  163. self::$valuesCache[$file] = $expiresAt;
  164. }
  165. [$expiresAt, $value] = $expiresAt;
  166. } elseif ($this->appendOnly) {
  167. $value = new LazyValue($file);
  168. }
  169. } catch (\ErrorException $e) {
  170. return false;
  171. } finally {
  172. restore_error_handler();
  173. }
  174. if ($this->appendOnly) {
  175. $now = 0;
  176. $this->values[$id] = $value;
  177. } else {
  178. $now = time();
  179. }
  180. return $now < $expiresAt;
  181. }
  182. /**
  183. * {@inheritdoc}
  184. */
  185. protected function doSave(array $values, int $lifetime)
  186. {
  187. $ok = true;
  188. $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
  189. $allowCompile = self::isSupported();
  190. foreach ($values as $key => $value) {
  191. unset($this->values[$key]);
  192. $isStaticValue = true;
  193. if (null === $value) {
  194. $value = "'N;'";
  195. } elseif (\is_object($value) || \is_array($value)) {
  196. try {
  197. $value = VarExporter::export($value, $isStaticValue);
  198. } catch (\Exception $e) {
  199. throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
  200. }
  201. } elseif (\is_string($value)) {
  202. // Wrap "N;" in a closure to not confuse it with an encoded `null`
  203. if ('N;' === $value) {
  204. $isStaticValue = false;
  205. }
  206. $value = var_export($value, true);
  207. } elseif (!\is_scalar($value)) {
  208. throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
  209. } else {
  210. $value = var_export($value, true);
  211. }
  212. $encodedKey = rawurlencode($key);
  213. if ($isStaticValue) {
  214. $value = "return [{$expiry}, {$value}];";
  215. } elseif ($this->appendOnly) {
  216. $value = "return [{$expiry}, static function () { return {$value}; }];";
  217. } else {
  218. // We cannot use a closure here because of https://bugs.php.net/76982
  219. $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
  220. $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
  221. }
  222. $file = $this->files[$key] = $this->getFile($key, true);
  223. // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
  224. $ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;
  225. if ($allowCompile) {
  226. @opcache_invalidate($file, true);
  227. @opcache_compile_file($file);
  228. }
  229. unset(self::$valuesCache[$file]);
  230. }
  231. if (!$ok && !is_writable($this->directory)) {
  232. throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
  233. }
  234. return $ok;
  235. }
  236. /**
  237. * {@inheritdoc}
  238. */
  239. protected function doClear(string $namespace)
  240. {
  241. $this->values = [];
  242. return $this->doCommonClear($namespace);
  243. }
  244. /**
  245. * {@inheritdoc}
  246. */
  247. protected function doDelete(array $ids)
  248. {
  249. foreach ($ids as $id) {
  250. unset($this->values[$id]);
  251. }
  252. return $this->doCommonDelete($ids);
  253. }
  254. protected function doUnlink(string $file)
  255. {
  256. unset(self::$valuesCache[$file]);
  257. if (self::isSupported()) {
  258. @opcache_invalidate($file, true);
  259. }
  260. return @unlink($file);
  261. }
  262. private function getFileKey(string $file): string
  263. {
  264. if (!$h = @fopen($file, 'r')) {
  265. return '';
  266. }
  267. $encodedKey = substr(fgets($h), 8);
  268. fclose($h);
  269. return rawurldecode(rtrim($encodedKey));
  270. }
  271. }
  272. /**
  273. * @internal
  274. */
  275. class LazyValue
  276. {
  277. public $file;
  278. public function __construct(string $file)
  279. {
  280. $this->file = $file;
  281. }
  282. }