ClassCollectionLoader.php 14 KB

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