annotate vendor/symfony/class-loader/ClassCollectionLoader.php @ 19:fa3358dc1485 tip

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