comparison vendor/symfony/class-loader/ClassCollectionLoader.php @ 0:4c8ae668cc8c

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