PageRenderTime 53ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php

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