ClassExistenceResource.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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\Resource;
  11. /**
  12. * ClassExistenceResource represents a class existence.
  13. * Freshness is only evaluated against resource existence.
  14. *
  15. * The resource must be a fully-qualified class name.
  16. *
  17. * @author Fabien Potencier <fabien@symfony.com>
  18. *
  19. * @final
  20. */
  21. class ClassExistenceResource implements SelfCheckingResourceInterface
  22. {
  23. private $resource;
  24. private $exists;
  25. private static $autoloadLevel = 0;
  26. private static $autoloadedClass;
  27. private static $existsCache = [];
  28. /**
  29. * @param string $resource The fully-qualified class name
  30. * @param bool|null $exists Boolean when the existency check has already been done
  31. */
  32. public function __construct(string $resource, bool $exists = null)
  33. {
  34. $this->resource = $resource;
  35. if (null !== $exists) {
  36. $this->exists = [(bool) $exists, null];
  37. }
  38. }
  39. /**
  40. * {@inheritdoc}
  41. */
  42. public function __toString(): string
  43. {
  44. return $this->resource;
  45. }
  46. /**
  47. * @return string The file path to the resource
  48. */
  49. public function getResource(): string
  50. {
  51. return $this->resource;
  52. }
  53. /**
  54. * {@inheritdoc}
  55. *
  56. * @throws \ReflectionException when a parent class/interface/trait is not found
  57. */
  58. public function isFresh(int $timestamp): bool
  59. {
  60. $loaded = class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
  61. if (null !== $exists = &self::$existsCache[$this->resource]) {
  62. if ($loaded) {
  63. $exists = [true, null];
  64. } elseif (0 >= $timestamp && !$exists[0] && null !== $exists[1]) {
  65. throw new \ReflectionException($exists[1]);
  66. }
  67. } elseif ([false, null] === $exists = [$loaded, null]) {
  68. if (!self::$autoloadLevel++) {
  69. spl_autoload_register(__CLASS__.'::throwOnRequiredClass');
  70. }
  71. $autoloadedClass = self::$autoloadedClass;
  72. self::$autoloadedClass = ltrim($this->resource, '\\');
  73. try {
  74. $exists[0] = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
  75. } catch (\Exception $e) {
  76. $exists[1] = $e->getMessage();
  77. try {
  78. self::throwOnRequiredClass($this->resource, $e);
  79. } catch (\ReflectionException $e) {
  80. if (0 >= $timestamp) {
  81. throw $e;
  82. }
  83. }
  84. } catch (\Throwable $e) {
  85. $exists[1] = $e->getMessage();
  86. throw $e;
  87. } finally {
  88. self::$autoloadedClass = $autoloadedClass;
  89. if (!--self::$autoloadLevel) {
  90. spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass');
  91. }
  92. }
  93. }
  94. if (null === $this->exists) {
  95. $this->exists = $exists;
  96. }
  97. return $this->exists[0] xor !$exists[0];
  98. }
  99. /**
  100. * @internal
  101. */
  102. public function __sleep(): array
  103. {
  104. if (null === $this->exists) {
  105. $this->isFresh(0);
  106. }
  107. return ['resource', 'exists'];
  108. }
  109. /**
  110. * @internal
  111. */
  112. public function __wakeup()
  113. {
  114. if (\is_bool($this->exists)) {
  115. $this->exists = [$this->exists, null];
  116. }
  117. }
  118. /**
  119. * Throws a reflection exception when the passed class does not exist but is required.
  120. *
  121. * A class is considered "not required" when it's loaded as part of a "class_exists" or similar check.
  122. *
  123. * This function can be used as an autoload function to throw a reflection
  124. * exception if the class was not found by previous autoload functions.
  125. *
  126. * A previous exception can be passed. In this case, the class is considered as being
  127. * required totally, so if it doesn't exist, a reflection exception is always thrown.
  128. * If it exists, the previous exception is rethrown.
  129. *
  130. * @throws \ReflectionException
  131. *
  132. * @internal
  133. */
  134. public static function throwOnRequiredClass(string $class, \Exception $previous = null)
  135. {
  136. // If the passed class is the resource being checked, we shouldn't throw.
  137. if (null === $previous && self::$autoloadedClass === $class) {
  138. return;
  139. }
  140. if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) {
  141. if (null !== $previous) {
  142. throw $previous;
  143. }
  144. return;
  145. }
  146. if ($previous instanceof \ReflectionException) {
  147. throw $previous;
  148. }
  149. $message = sprintf('Class "%s" not found.', $class);
  150. if (self::$autoloadedClass !== $class) {
  151. $message = substr_replace($message, sprintf(' while loading "%s"', self::$autoloadedClass), -1, 0);
  152. }
  153. if (null !== $previous) {
  154. $message = $previous->getMessage();
  155. }
  156. $e = new \ReflectionException($message, 0, $previous);
  157. if (null !== $previous) {
  158. throw $e;
  159. }
  160. $trace = debug_backtrace();
  161. $autoloadFrame = [
  162. 'function' => 'spl_autoload_call',
  163. 'args' => [$class],
  164. ];
  165. if (\PHP_VERSION_ID >= 80000 && isset($trace[1])) {
  166. $callerFrame = $trace[1];
  167. $i = 2;
  168. } elseif (false !== $i = array_search($autoloadFrame, $trace, true)) {
  169. $callerFrame = $trace[++$i];
  170. } else {
  171. throw $e;
  172. }
  173. if (isset($callerFrame['function']) && !isset($callerFrame['class'])) {
  174. switch ($callerFrame['function']) {
  175. case 'get_class_methods':
  176. case 'get_class_vars':
  177. case 'get_parent_class':
  178. case 'is_a':
  179. case 'is_subclass_of':
  180. case 'class_exists':
  181. case 'class_implements':
  182. case 'class_parents':
  183. case 'trait_exists':
  184. case 'defined':
  185. case 'interface_exists':
  186. case 'method_exists':
  187. case 'property_exists':
  188. case 'is_callable':
  189. return;
  190. }
  191. $props = [
  192. 'file' => $callerFrame['file'] ?? null,
  193. 'line' => $callerFrame['line'] ?? null,
  194. 'trace' => \array_slice($trace, 1 + $i),
  195. ];
  196. foreach ($props as $p => $v) {
  197. if (null !== $v) {
  198. $r = new \ReflectionProperty(\Exception::class, $p);
  199. $r->setAccessible(true);
  200. $r->setValue($e, $v);
  201. }
  202. }
  203. }
  204. throw $e;
  205. }
  206. }