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 }
|