ResourceCheckerConfigCache.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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\Config;
  11. use Symfony\Component\Config\Resource\ResourceInterface;
  12. use Symfony\Component\Filesystem\Exception\IOException;
  13. use Symfony\Component\Filesystem\Filesystem;
  14. /**
  15. * ResourceCheckerConfigCache uses instances of ResourceCheckerInterface
  16. * to check whether cached data is still fresh.
  17. *
  18. * @author Matthias Pigulla <mp@webfactory.de>
  19. */
  20. class ResourceCheckerConfigCache implements ConfigCacheInterface
  21. {
  22. /**
  23. * @var string
  24. */
  25. private $file;
  26. /**
  27. * @var iterable|ResourceCheckerInterface[]
  28. */
  29. private $resourceCheckers;
  30. /**
  31. * @param string $file The absolute cache path
  32. * @param iterable|ResourceCheckerInterface[] $resourceCheckers The ResourceCheckers to use for the freshness check
  33. */
  34. public function __construct(string $file, iterable $resourceCheckers = [])
  35. {
  36. $this->file = $file;
  37. $this->resourceCheckers = $resourceCheckers;
  38. }
  39. /**
  40. * {@inheritdoc}
  41. */
  42. public function getPath()
  43. {
  44. return $this->file;
  45. }
  46. /**
  47. * Checks if the cache is still fresh.
  48. *
  49. * This implementation will make a decision solely based on the ResourceCheckers
  50. * passed in the constructor.
  51. *
  52. * The first ResourceChecker that supports a given resource is considered authoritative.
  53. * Resources with no matching ResourceChecker will silently be ignored and considered fresh.
  54. *
  55. * @return bool true if the cache is fresh, false otherwise
  56. */
  57. public function isFresh()
  58. {
  59. if (!is_file($this->file)) {
  60. return false;
  61. }
  62. if ($this->resourceCheckers instanceof \Traversable && !$this->resourceCheckers instanceof \Countable) {
  63. $this->resourceCheckers = iterator_to_array($this->resourceCheckers);
  64. }
  65. if (!\count($this->resourceCheckers)) {
  66. return true; // shortcut - if we don't have any checkers we don't need to bother with the meta file at all
  67. }
  68. $metadata = $this->getMetaFile();
  69. if (!is_file($metadata)) {
  70. return false;
  71. }
  72. $meta = $this->safelyUnserialize($metadata);
  73. if (false === $meta) {
  74. return false;
  75. }
  76. $time = filemtime($this->file);
  77. foreach ($meta as $resource) {
  78. /* @var ResourceInterface $resource */
  79. foreach ($this->resourceCheckers as $checker) {
  80. if (!$checker->supports($resource)) {
  81. continue; // next checker
  82. }
  83. if ($checker->isFresh($resource, $time)) {
  84. break; // no need to further check this resource
  85. }
  86. return false; // cache is stale
  87. }
  88. // no suitable checker found, ignore this resource
  89. }
  90. return true;
  91. }
  92. /**
  93. * Writes cache.
  94. *
  95. * @param string $content The content to write in the cache
  96. * @param ResourceInterface[] $metadata An array of metadata
  97. *
  98. * @throws \RuntimeException When cache file can't be written
  99. */
  100. public function write(string $content, array $metadata = null)
  101. {
  102. $mode = 0666;
  103. $umask = umask();
  104. $filesystem = new Filesystem();
  105. $filesystem->dumpFile($this->file, $content);
  106. try {
  107. $filesystem->chmod($this->file, $mode, $umask);
  108. } catch (IOException $e) {
  109. // discard chmod failure (some filesystem may not support it)
  110. }
  111. if (null !== $metadata) {
  112. $filesystem->dumpFile($this->getMetaFile(), serialize($metadata));
  113. try {
  114. $filesystem->chmod($this->getMetaFile(), $mode, $umask);
  115. } catch (IOException $e) {
  116. // discard chmod failure (some filesystem may not support it)
  117. }
  118. }
  119. if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
  120. @opcache_invalidate($this->file, true);
  121. }
  122. }
  123. /**
  124. * Gets the meta file path.
  125. */
  126. private function getMetaFile(): string
  127. {
  128. return $this->file.'.meta';
  129. }
  130. private function safelyUnserialize(string $file)
  131. {
  132. $meta = false;
  133. $content = file_get_contents($file);
  134. $signalingException = new \UnexpectedValueException();
  135. $prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback');
  136. $prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) {
  137. if (__FILE__ === $file) {
  138. throw $signalingException;
  139. }
  140. return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
  141. });
  142. try {
  143. $meta = unserialize($content);
  144. } catch (\Throwable $e) {
  145. if ($e !== $signalingException) {
  146. throw $e;
  147. }
  148. } finally {
  149. restore_error_handler();
  150. ini_set('unserialize_callback_func', $prevUnserializeHandler);
  151. }
  152. return $meta;
  153. }
  154. /**
  155. * @internal
  156. */
  157. public static function handleUnserializeCallback($class)
  158. {
  159. trigger_error('Class not found: '.$class);
  160. }
  161. }