4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\ClassLoader;
15 * ClassCollectionLoader.
17 * @author Fabien Potencier <fabien@symfony.com>
19 class ClassCollectionLoader
21 private static $loaded;
23 private static $useTokenizer = true;
26 * Loads a list of classes and caches them in one big file.
28 * @param array $classes An array of classes to load
29 * @param string $cacheDir A cache directory
30 * @param string $name The cache name prefix
31 * @param bool $autoReload Whether to flush the cache when the cache is stale or not
32 * @param bool $adaptive Whether to remove already declared classes or not
33 * @param string $extension File extension of the resulting file
35 * @throws \InvalidArgumentException When class can't be loaded
37 public static function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php')
39 // each $name can only be loaded once per PHP process
40 if (isset(self::$loaded[$name])) {
44 self::$loaded[$name] = true;
47 $declared = array_merge(get_declared_classes(), get_declared_interfaces());
48 if (function_exists('get_declared_traits')) {
49 $declared = array_merge($declared, get_declared_traits());
52 // don't include already declared classes
53 $classes = array_diff($classes, $declared);
55 // the cache is different depending on which classes are already declared
56 $name = $name.'-'.substr(hash('sha256', implode('|', $classes)), 0, 5);
59 $classes = array_unique($classes);
61 // cache the core classes
62 if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
63 throw new \RuntimeException(sprintf('Class Collection Loader was not able to create directory "%s"', $cacheDir));
65 $cacheDir = rtrim(realpath($cacheDir) ?: $cacheDir, '/'.DIRECTORY_SEPARATOR);
66 $cache = $cacheDir.'/'.$name.$extension;
71 $metadata = $cache.'.meta';
72 if (!is_file($metadata) || !is_file($cache)) {
75 $time = filemtime($cache);
76 $meta = unserialize(file_get_contents($metadata));
81 if ($meta[1] != $classes) {
84 foreach ($meta[0] as $resource) {
85 if (!is_file($resource) || filemtime($resource) > $time) {
95 if (!$reload && file_exists($cache)) {
101 $declared = array_merge(get_declared_classes(), get_declared_interfaces());
102 if (function_exists('get_declared_traits')) {
103 $declared = array_merge($declared, get_declared_traits());
107 $spacesRegex = '(?:\s*+(?:(?:\#|//)[^\n]*+\n|/\*(?:(?<!\*/).)++)?+)*+';
108 $dontInlineRegex = <<<REGEX
110 ^<\?php\s.declare.\(.strict_types.=.1.\).;
111 | \b__halt_compiler.\(.\)
112 | \b__(?:DIR|FILE)__\b
115 $dontInlineRegex = str_replace('.', $spacesRegex, $dontInlineRegex);
117 $cacheDir = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $cacheDir));
120 foreach (self::getOrderedClasses($classes) as $class) {
121 if (in_array($class->getName(), $declared)) {
125 $files[] = $file = $class->getFileName();
126 $c = file_get_contents($file);
128 if (preg_match($dontInlineRegex, $c)) {
129 $file = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $file));
131 for ($i = 0; isset($file[$i], $cacheDir[$i]); ++$i) {
132 if ($file[$i] !== $cacheDir[$i]) {
137 $file = var_export(implode('/', $file), true);
139 $file = array_slice($file, $i);
140 $file = str_repeat('../', count($cacheDir) - $i).implode('/', $file);
141 $file = '__DIR__.'.var_export('/'.$file, true);
144 $c = "\nnamespace {require $file;}";
146 $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', $c);
148 // fakes namespace declaration for global code
149 if (!$class->inNamespace()) {
150 $c = "\nnamespace\n{\n".$c."\n}\n";
153 $c = self::fixNamespaceDeclarations('<?php '.$c);
154 $c = preg_replace('/^\s*<\?php/', '', $c);
159 self::writeCacheFile($cache, '<?php '.$content);
162 // save the resources
163 self::writeCacheFile($metadata, serialize(array($files, $classes)));
168 * Adds brackets around each namespace if it's not already the case.
170 * @param string $source Namespace string
172 * @return string Namespaces with brackets
174 public static function fixNamespaceDeclarations($source)
176 if (!function_exists('token_get_all') || !self::$useTokenizer) {
177 if (preg_match('/(^|\s)namespace(.*?)\s*;/', $source)) {
178 $source = preg_replace('/(^|\s)namespace(.*?)\s*;/', "$1namespace$2\n{", $source)."}\n";
186 $inNamespace = false;
187 $tokens = token_get_all($source);
189 for ($i = 0; isset($tokens[$i]); ++$i) {
190 $token = $tokens[$i];
191 if (!isset($token[1]) || 'b"' === $token) {
193 } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
196 } elseif (T_NAMESPACE === $token[0]) {
200 $rawChunk .= $token[1];
202 // namespace name and whitespaces
203 while (isset($tokens[++$i][1]) && in_array($tokens[$i][0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
204 $rawChunk .= $tokens[$i][1];
206 if ('{' === $tokens[$i]) {
207 $inNamespace = false;
210 $rawChunk = rtrim($rawChunk)."\n{";
213 } elseif (T_START_HEREDOC === $token[0]) {
214 $output .= self::compressCode($rawChunk).$token[1];
216 $token = $tokens[++$i];
217 $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token;
218 } while ($token[0] !== T_END_HEREDOC);
221 } elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) {
222 $output .= self::compressCode($rawChunk).$token[1];
225 $rawChunk .= $token[1];
233 $output .= self::compressCode($rawChunk);
235 if (\PHP_VERSION_ID >= 70000) {
236 // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
237 unset($tokens, $rawChunk);
245 * This method is only useful for testing.
247 public static function enableTokenizer($bool)
249 self::$useTokenizer = (bool) $bool;
253 * Strips leading & trailing ws, multiple EOL, multiple ws.
255 * @param string $code Original PHP code
257 * @return string compressed code
259 private static function compressCode($code)
262 array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'),
263 array('', '', "\n", ' '),
269 * Writes a cache file.
271 * @param string $file Filename
272 * @param string $content Temporary file content
274 * @throws \RuntimeException when a cache file cannot be written
276 private static function writeCacheFile($file, $content)
278 $dir = dirname($file);
279 if (!is_writable($dir)) {
280 throw new \RuntimeException(sprintf('Cache directory "%s" is not writable.', $dir));
283 $tmpFile = tempnam($dir, basename($file));
285 if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) {
286 @chmod($file, 0666 & ~umask());
291 throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file));
295 * Gets an ordered array of passed classes including all their dependencies.
297 * @param array $classes
299 * @return \ReflectionClass[] An array of sorted \ReflectionClass instances (dependencies added if needed)
301 * @throws \InvalidArgumentException When a class can't be loaded
303 private static function getOrderedClasses(array $classes)
306 self::$seen = array();
307 foreach ($classes as $class) {
309 $reflectionClass = new \ReflectionClass($class);
310 } catch (\ReflectionException $e) {
311 throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class));
314 $map = array_merge($map, self::getClassHierarchy($reflectionClass));
320 private static function getClassHierarchy(\ReflectionClass $class)
322 if (isset(self::$seen[$class->getName()])) {
326 self::$seen[$class->getName()] = true;
328 $classes = array($class);
330 while (($parent = $parent->getParentClass()) && $parent->isUserDefined() && !isset(self::$seen[$parent->getName()])) {
331 self::$seen[$parent->getName()] = true;
333 array_unshift($classes, $parent);
338 if (method_exists('ReflectionClass', 'getTraits')) {
339 foreach ($classes as $c) {
340 foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) {
348 return array_merge(self::getInterfaces($class), $traits, $classes);
351 private static function getInterfaces(\ReflectionClass $class)
355 foreach ($class->getInterfaces() as $interface) {
356 $classes = array_merge($classes, self::getInterfaces($interface));
359 if ($class->isUserDefined() && $class->isInterface() && !isset(self::$seen[$class->getName()])) {
360 self::$seen[$class->getName()] = true;
368 private static function computeTraitDeps(\ReflectionClass $class)
370 $traits = $class->getTraits();
371 $deps = array($class->getName() => $traits);
372 while ($trait = array_pop($traits)) {
373 if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) {
374 self::$seen[$trait->getName()] = true;
375 $traitDeps = $trait->getTraits();
376 $deps[$trait->getName()] = $traitDeps;
377 $traits = array_merge($traits, $traitDeps);
385 * Dependencies resolution.
387 * This function does not check for circular dependencies as it should never
388 * occur with PHP traits.
390 * @param array $tree The dependency tree
391 * @param \ReflectionClass $node The node
392 * @param \ArrayObject $resolved An array of already resolved dependencies
393 * @param \ArrayObject $unresolved An array of dependencies to be resolved
395 * @return \ArrayObject The dependencies for the given node
397 * @throws \RuntimeException if a circular dependency is detected
399 private static function resolveDependencies(array $tree, $node, \ArrayObject $resolved = null, \ArrayObject $unresolved = null)
401 if (null === $resolved) {
402 $resolved = new \ArrayObject();
404 if (null === $unresolved) {
405 $unresolved = new \ArrayObject();
407 $nodeName = $node->getName();
409 if (isset($tree[$nodeName])) {
410 $unresolved[$nodeName] = $node;
411 foreach ($tree[$nodeName] as $dependency) {
412 if (!$resolved->offsetExists($dependency->getName())) {
413 self::resolveDependencies($tree, $dependency, $resolved, $unresolved);
416 $resolved[$nodeName] = $node;
417 unset($unresolved[$nodeName]);