ClassCollectionLoader.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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\ClassLoader;
  11. if (\PHP_VERSION_ID >= 70000) {
  12. @trigger_error('The '.__NAMESPACE__.'\ClassCollectionLoader class is deprecated since Symfony 3.3 and will be removed in 4.0.', E_USER_DEPRECATED);
  13. }
  14. /**
  15. * ClassCollectionLoader.
  16. *
  17. * @author Fabien Potencier <fabien@symfony.com>
  18. *
  19. * @deprecated since version 3.3, to be removed in 4.0.
  20. */
  21. class ClassCollectionLoader
  22. {
  23. private static $loaded;
  24. private static $seen;
  25. private static $useTokenizer = true;
  26. /**
  27. * Loads a list of classes and caches them in one big file.
  28. *
  29. * @param array $classes An array of classes to load
  30. * @param string $cacheDir A cache directory
  31. * @param string $name The cache name prefix
  32. * @param bool $autoReload Whether to flush the cache when the cache is stale or not
  33. * @param bool $adaptive Whether to remove already declared classes or not
  34. * @param string $extension File extension of the resulting file
  35. *
  36. * @throws \InvalidArgumentException When class can't be loaded
  37. */
  38. public static function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php')
  39. {
  40. // each $name can only be loaded once per PHP process
  41. if (isset(self::$loaded[$name])) {
  42. return;
  43. }
  44. self::$loaded[$name] = true;
  45. if ($adaptive) {
  46. $declared = array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits());
  47. // don't include already declared classes
  48. $classes = array_diff($classes, $declared);
  49. // the cache is different depending on which classes are already declared
  50. $name .= '-'.substr(hash('sha256', implode('|', $classes)), 0, 5);
  51. }
  52. $classes = array_unique($classes);
  53. // cache the core classes
  54. if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
  55. throw new \RuntimeException(sprintf('Class Collection Loader was not able to create directory "%s"', $cacheDir));
  56. }
  57. $cacheDir = rtrim(realpath($cacheDir) ?: $cacheDir, '/'.\DIRECTORY_SEPARATOR);
  58. $cache = $cacheDir.'/'.$name.$extension;
  59. // auto-reload
  60. $reload = false;
  61. if ($autoReload) {
  62. $metadata = $cache.'.meta';
  63. if (!is_file($metadata) || !is_file($cache)) {
  64. $reload = true;
  65. } else {
  66. $time = filemtime($cache);
  67. $meta = unserialize(file_get_contents($metadata));
  68. sort($meta[1]);
  69. sort($classes);
  70. if ($meta[1] != $classes) {
  71. $reload = true;
  72. } else {
  73. foreach ($meta[0] as $resource) {
  74. if (!is_file($resource) || filemtime($resource) > $time) {
  75. $reload = true;
  76. break;
  77. }
  78. }
  79. }
  80. }
  81. }
  82. if (!$reload && file_exists($cache)) {
  83. require_once $cache;
  84. return;
  85. }
  86. if (!$adaptive) {
  87. $declared = array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits());
  88. }
  89. $files = self::inline($classes, $cache, $declared);
  90. if ($autoReload) {
  91. // save the resources
  92. self::writeCacheFile($metadata, serialize([array_values($files), $classes]));
  93. }
  94. }
  95. /**
  96. * Generates a file where classes and their parents are inlined.
  97. *
  98. * @param array $classes An array of classes to load
  99. * @param string $cache The file where classes are inlined
  100. * @param array $excluded An array of classes that won't be inlined
  101. *
  102. * @return array The source map of inlined classes, with classes as keys and files as values
  103. *
  104. * @throws \RuntimeException When class can't be loaded
  105. */
  106. public static function inline($classes, $cache, array $excluded)
  107. {
  108. $declared = [];
  109. foreach (self::getOrderedClasses($excluded) as $class) {
  110. $declared[$class->getName()] = true;
  111. }
  112. // cache the core classes
  113. $cacheDir = \dirname($cache);
  114. if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
  115. throw new \RuntimeException(sprintf('Class Collection Loader was not able to create directory "%s"', $cacheDir));
  116. }
  117. $spacesRegex = '(?:\s*+(?:(?:\#|//)[^\n]*+\n|/\*(?:(?<!\*/).)++)?+)*+';
  118. $dontInlineRegex = <<<REGEX
  119. '(?:
  120. ^<\?php\s.declare.\(.strict_types.=.1.\).;
  121. | \b__halt_compiler.\(.\)
  122. | \b__(?:DIR|FILE)__\b
  123. )'isx
  124. REGEX;
  125. $dontInlineRegex = str_replace('.', $spacesRegex, $dontInlineRegex);
  126. $cacheDir = explode('/', str_replace(\DIRECTORY_SEPARATOR, '/', $cacheDir));
  127. $files = [];
  128. $content = '';
  129. foreach (self::getOrderedClasses($classes) as $class) {
  130. if (isset($declared[$class->getName()])) {
  131. continue;
  132. }
  133. $declared[$class->getName()] = true;
  134. $files[$class->getName()] = $file = $class->getFileName();
  135. $c = file_get_contents($file);
  136. if (preg_match($dontInlineRegex, $c)) {
  137. $file = explode('/', str_replace(\DIRECTORY_SEPARATOR, '/', $file));
  138. for ($i = 0; isset($file[$i], $cacheDir[$i]); ++$i) {
  139. if ($file[$i] !== $cacheDir[$i]) {
  140. break;
  141. }
  142. }
  143. if (1 >= $i) {
  144. $file = var_export(implode('/', $file), true);
  145. } else {
  146. $file = \array_slice($file, $i);
  147. $file = str_repeat('../', \count($cacheDir) - $i).implode('/', $file);
  148. $file = '__DIR__.'.var_export('/'.$file, true);
  149. }
  150. $c = "\nnamespace {require $file;}";
  151. } else {
  152. $c = preg_replace(['/^\s*<\?php/', '/\?>\s*$/'], '', $c);
  153. // fakes namespace declaration for global code
  154. if (!$class->inNamespace()) {
  155. $c = "\nnamespace\n{\n".$c."\n}\n";
  156. }
  157. $c = self::fixNamespaceDeclarations('<?php '.$c);
  158. $c = preg_replace('/^\s*<\?php/', '', $c);
  159. }
  160. $content .= $c;
  161. }
  162. self::writeCacheFile($cache, '<?php '.$content);
  163. return $files;
  164. }
  165. /**
  166. * Adds brackets around each namespace if it's not already the case.
  167. *
  168. * @param string $source Namespace string
  169. *
  170. * @return string Namespaces with brackets
  171. */
  172. public static function fixNamespaceDeclarations($source)
  173. {
  174. if (!\function_exists('token_get_all') || !self::$useTokenizer) {
  175. if (preg_match('/(^|\s)namespace(.*?)\s*;/', $source)) {
  176. $source = preg_replace('/(^|\s)namespace(.*?)\s*;/', "$1namespace$2\n{", $source)."}\n";
  177. }
  178. return $source;
  179. }
  180. $rawChunk = '';
  181. $output = '';
  182. $inNamespace = false;
  183. $tokens = token_get_all($source);
  184. for ($i = 0; isset($tokens[$i]); ++$i) {
  185. $token = $tokens[$i];
  186. if (!isset($token[1]) || 'b"' === $token) {
  187. $rawChunk .= $token;
  188. } elseif (\in_array($token[0], [T_COMMENT, T_DOC_COMMENT])) {
  189. // strip comments
  190. continue;
  191. } elseif (T_NAMESPACE === $token[0]) {
  192. if ($inNamespace) {
  193. $rawChunk .= "}\n";
  194. }
  195. $rawChunk .= $token[1];
  196. // namespace name and whitespaces
  197. while (isset($tokens[++$i][1]) && \in_array($tokens[$i][0], [T_WHITESPACE, T_NS_SEPARATOR, T_STRING])) {
  198. $rawChunk .= $tokens[$i][1];
  199. }
  200. if ('{' === $tokens[$i]) {
  201. $inNamespace = false;
  202. --$i;
  203. } else {
  204. $rawChunk = rtrim($rawChunk)."\n{";
  205. $inNamespace = true;
  206. }
  207. } elseif (T_START_HEREDOC === $token[0]) {
  208. $output .= self::compressCode($rawChunk).$token[1];
  209. do {
  210. $token = $tokens[++$i];
  211. $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token;
  212. } while (T_END_HEREDOC !== $token[0]);
  213. $output .= "\n";
  214. $rawChunk = '';
  215. } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) {
  216. $output .= self::compressCode($rawChunk).$token[1];
  217. $rawChunk = '';
  218. } else {
  219. $rawChunk .= $token[1];
  220. }
  221. }
  222. if ($inNamespace) {
  223. $rawChunk .= "}\n";
  224. }
  225. $output .= self::compressCode($rawChunk);
  226. if (\PHP_VERSION_ID >= 70000) {
  227. // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
  228. unset($tokens, $rawChunk);
  229. gc_mem_caches();
  230. }
  231. return $output;
  232. }
  233. /**
  234. * This method is only useful for testing.
  235. */
  236. public static function enableTokenizer($bool)
  237. {
  238. self::$useTokenizer = (bool) $bool;
  239. }
  240. /**
  241. * Strips leading & trailing ws, multiple EOL, multiple ws.
  242. *
  243. * @param string $code Original PHP code
  244. *
  245. * @return string compressed code
  246. */
  247. private static function compressCode($code)
  248. {
  249. return preg_replace(
  250. ['/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'],
  251. ['', '', "\n", ' '],
  252. $code
  253. );
  254. }
  255. /**
  256. * Writes a cache file.
  257. *
  258. * @param string $file Filename
  259. * @param string $content Temporary file content
  260. *
  261. * @throws \RuntimeException when a cache file cannot be written
  262. */
  263. private static function writeCacheFile($file, $content)
  264. {
  265. $dir = \dirname($file);
  266. if (!is_writable($dir)) {
  267. throw new \RuntimeException(sprintf('Cache directory "%s" is not writable.', $dir));
  268. }
  269. $tmpFile = tempnam($dir, basename($file));
  270. if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) {
  271. @chmod($file, 0666 & ~umask());
  272. return;
  273. }
  274. throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file));
  275. }
  276. /**
  277. * Gets an ordered array of passed classes including all their dependencies.
  278. *
  279. * @return \ReflectionClass[] An array of sorted \ReflectionClass instances (dependencies added if needed)
  280. *
  281. * @throws \InvalidArgumentException When a class can't be loaded
  282. */
  283. private static function getOrderedClasses(array $classes)
  284. {
  285. $map = [];
  286. self::$seen = [];
  287. foreach ($classes as $class) {
  288. try {
  289. $reflectionClass = new \ReflectionClass($class);
  290. } catch (\ReflectionException $e) {
  291. throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class));
  292. }
  293. $map = array_merge($map, self::getClassHierarchy($reflectionClass));
  294. }
  295. return $map;
  296. }
  297. private static function getClassHierarchy(\ReflectionClass $class)
  298. {
  299. if (isset(self::$seen[$class->getName()])) {
  300. return [];
  301. }
  302. self::$seen[$class->getName()] = true;
  303. $classes = [$class];
  304. $parent = $class;
  305. while (($parent = $parent->getParentClass()) && $parent->isUserDefined() && !isset(self::$seen[$parent->getName()])) {
  306. self::$seen[$parent->getName()] = true;
  307. array_unshift($classes, $parent);
  308. }
  309. $traits = [];
  310. foreach ($classes as $c) {
  311. foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) {
  312. if ($trait !== $c) {
  313. $traits[] = $trait;
  314. }
  315. }
  316. }
  317. return array_merge(self::getInterfaces($class), $traits, $classes);
  318. }
  319. private static function getInterfaces(\ReflectionClass $class)
  320. {
  321. $classes = [];
  322. foreach ($class->getInterfaces() as $interface) {
  323. $classes = array_merge($classes, self::getInterfaces($interface));
  324. }
  325. if ($class->isUserDefined() && $class->isInterface() && !isset(self::$seen[$class->getName()])) {
  326. self::$seen[$class->getName()] = true;
  327. $classes[] = $class;
  328. }
  329. return $classes;
  330. }
  331. private static function computeTraitDeps(\ReflectionClass $class)
  332. {
  333. $traits = $class->getTraits();
  334. $deps = [$class->getName() => $traits];
  335. while ($trait = array_pop($traits)) {
  336. if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) {
  337. self::$seen[$trait->getName()] = true;
  338. $traitDeps = $trait->getTraits();
  339. $deps[$trait->getName()] = $traitDeps;
  340. $traits = array_merge($traits, $traitDeps);
  341. }
  342. }
  343. return $deps;
  344. }
  345. /**
  346. * Dependencies resolution.
  347. *
  348. * This function does not check for circular dependencies as it should never
  349. * occur with PHP traits.
  350. *
  351. * @param array $tree The dependency tree
  352. * @param \ReflectionClass $node The node
  353. * @param \ArrayObject $resolved An array of already resolved dependencies
  354. * @param \ArrayObject $unresolved An array of dependencies to be resolved
  355. *
  356. * @return \ArrayObject The dependencies for the given node
  357. *
  358. * @throws \RuntimeException if a circular dependency is detected
  359. */
  360. private static function resolveDependencies(array $tree, $node, \ArrayObject $resolved = null, \ArrayObject $unresolved = null)
  361. {
  362. if (null === $resolved) {
  363. $resolved = new \ArrayObject();
  364. }
  365. if (null === $unresolved) {
  366. $unresolved = new \ArrayObject();
  367. }
  368. $nodeName = $node->getName();
  369. if (isset($tree[$nodeName])) {
  370. $unresolved[$nodeName] = $node;
  371. foreach ($tree[$nodeName] as $dependency) {
  372. if (!$resolved->offsetExists($dependency->getName())) {
  373. self::resolveDependencies($tree, $dependency, $resolved, $unresolved);
  374. }
  375. }
  376. $resolved[$nodeName] = $node;
  377. unset($unresolved[$nodeName]);
  378. }
  379. return $resolved;
  380. }
  381. }