Completed
Push — master ( b9da55...1f3f64 )
by Ingo
28s
created

ClassManifest   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 707
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 707
rs 4.4444
c 0
b 0
f 0
wmc 90
lcom 1
cbo 4

25 Methods

Rating   Name   Duplication   Size   Complexity  
A get_class_parser() 0 21 1
A get_namespaced_class_parser() 0 23 1
A get_trait_parser() 0 8 1
A get_namespace_parser() 0 11 1
A get_interface_parser() 0 8 1
A get_imported_namespace_parser() 0 17 1
B __construct() 0 22 5
A getItemPath() 0 15 4
A getClasses() 0 4 1
A getClassNames() 0 4 1
A getTraitNames() 0 4 1
A getDescendants() 0 4 1
A getDescendantsOf() 0 14 3
A getInterfaces() 0 4 1
A getImplementors() 0 4 1
A getImplementorsOf() 0 10 2
A getConfigs() 0 4 1
B getModules() 0 19 5
A getOwnerModule() 0 19 4
B regenerate() 0 39 4
A handleDir() 0 6 2
C findClassOrInterfaceFromCandidateImports() 0 60 9
B getImportsFromTokens() 0 34 6
F handleFile() 0 139 30
A coalesceDescendants() 0 20 3

How to fix   Complexity   

Complex Class

Complex classes like ClassManifest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassManifest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use Exception;
6
use SilverStripe\Control\Director;
7
8
/**
9
 * A utility class which builds a manifest of all classes, interfaces and some
10
 * additional items present in a directory, and caches it.
11
 *
12
 * It finds the following information:
13
 *   - Class and interface names and paths.
14
 *   - All direct and indirect descendants of a class.
15
 *   - All implementors of an interface.
16
 *   - All module configuration files.
17
 */
18
class ClassManifest
19
{
20
21
    const CONF_FILE = '_config.php';
22
    const CONF_DIR = '_config';
23
24
    protected $base;
25
    protected $tests;
26
27
    /**
28
     * @var ManifestCache
29
     */
30
    protected $cache;
31
32
    /**
33
     * @var string
34
     */
35
    protected $cacheKey;
36
37
    protected $classes      = array();
38
    protected $roots        = array();
39
    protected $children     = array();
40
    protected $descendants  = array();
41
    protected $interfaces   = array();
42
    protected $implementors = array();
43
    protected $configs      = array();
44
    protected $configDirs   = array();
45
    protected $traits       = array();
46
47
    /**
48
     * @return TokenisedRegularExpression
49
     */
50
    public static function get_class_parser()
51
    {
52
        return new TokenisedRegularExpression(array(
53
            0 => T_CLASS,
54
            1 => array(T_WHITESPACE, 'optional' => true),
55
            2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'),
56
            3 => array(T_WHITESPACE, 'optional' => true),
57
            4 => T_EXTENDS,
58
            5 => array(T_WHITESPACE, 'optional' => true),
59
            6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14),
60
            7 => array(T_WHITESPACE, 'optional' => true),
61
            8 => T_IMPLEMENTS,
62
            9 => array(T_WHITESPACE, 'optional' => true),
63
            10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'),
64
            11 => array(T_WHITESPACE, 'optional' => true),
65
            12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'),
66
            13 => array(T_WHITESPACE, 'can_jump_to' => 10),
67
            14 => array(T_WHITESPACE, 'optional' => true),
68
            15 => '{',
69
        ));
70
    }
71
72
    /**
73
     * @return TokenisedRegularExpression
74
     */
75
    public static function get_namespaced_class_parser()
76
    {
77
        return new TokenisedRegularExpression(array(
78
            0 => T_CLASS,
79
            1 => array(T_WHITESPACE, 'optional' => true),
80
            2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'),
81
            3 => array(T_WHITESPACE, 'optional' => true),
82
            4 => T_EXTENDS,
83
            5 => array(T_WHITESPACE, 'optional' => true),
84
            6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true),
85
            7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)),
86
            8 => array(T_WHITESPACE, 'optional' => true),
87
            9 => T_IMPLEMENTS,
88
            10 => array(T_WHITESPACE, 'optional' => true),
89
            11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true),
90
            12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'),
91
            13 => array(T_WHITESPACE, 'optional' => true),
92
            14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'),
93
            15 => array(T_WHITESPACE, 'can_jump_to' => 11),
94
            16 => array(T_WHITESPACE, 'optional' => true),
95
            17 => '{',
96
        ));
97
    }
98
99
    /**
100
     * @return TokenisedRegularExpression
101
     */
102
    public static function get_trait_parser()
103
    {
104
        return new TokenisedRegularExpression(array(
105
            0 => T_TRAIT,
106
            1 => array(T_WHITESPACE, 'optional' => true),
107
            2 => array(T_STRING, 'save_to' => 'traitName')
108
        ));
109
    }
110
111
    /**
112
     * @return TokenisedRegularExpression
113
     */
114
    public static function get_namespace_parser()
115
    {
116
        return new TokenisedRegularExpression(array(
117
            0 => T_NAMESPACE,
118
            1 => array(T_WHITESPACE, 'optional' => true),
119
            2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true),
120
            3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2),
121
            4 => array(T_WHITESPACE, 'optional' => true),
122
            5 => ';',
123
        ));
124
    }
125
126
    /**
127
     * @return TokenisedRegularExpression
128
     */
129
    public static function get_interface_parser()
130
    {
131
        return new TokenisedRegularExpression(array(
132
            0 => T_INTERFACE,
133
            1 => array(T_WHITESPACE, 'optional' => true),
134
            2 => array(T_STRING, 'save_to' => 'interfaceName')
135
        ));
136
    }
137
138
    /**
139
     * Create a {@link TokenisedRegularExpression} that extracts the namespaces imported with the 'use' keyword
140
     *
141
     * This searches symbols for a `use` followed by 1 or more namespaces which are optionally aliased using the `as`
142
     * keyword. The relevant matching tokens are added one-by-one into an array (using `save_to` param).
143
     *
144
     * eg: use Namespace\ClassName as Alias, OtherNamespace\ClassName;
145
     *
146
     * @return TokenisedRegularExpression
147
     */
148
    public static function get_imported_namespace_parser()
149
    {
150
        return new TokenisedRegularExpression(array(
151
            0 => T_USE,
152
            1 => array(T_WHITESPACE, 'optional' => true),
153
            2 => array(T_NS_SEPARATOR, 'save_to' => 'importString[]', 'optional' => true),
154
            3 => array(T_STRING, 'save_to' => 'importString[]', 'can_jump_to' => array(2, 8)),
155
            4 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
156
            5 => array(T_AS, 'save_to' => 'importString[]'),
157
            6 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
158
            7 => array(T_STRING, 'save_to' => 'importString[]'),
159
            8 => array(T_WHITESPACE, 'optional' => true),
160
            9 => array(',', 'save_to' => 'importString[]', 'optional' => true, 'can_jump_to' => 2),
161
            10 => array(T_WHITESPACE, 'optional' => true, 'can_jump_to' => 2),
162
            11 => ';',
163
        ));
164
    }
165
166
    /**
167
     * Constructs and initialises a new class manifest, either loading the data
168
     * from the cache or re-scanning for classes.
169
     *
170
     * @param string $base The manifest base path.
171
     * @param bool   $includeTests Include the contents of "tests" directories.
172
     * @param bool   $forceRegen Force the manifest to be regenerated.
173
     * @param bool   $cache If the manifest is regenerated, cache it.
174
     */
175
    public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true)
176
    {
177
        $this->base  = $base;
178
        $this->tests = $includeTests;
179
180
        $cacheClass = getenv('SS_MANIFESTCACHE') ?: 'SilverStripe\\Core\\Manifest\\ManifestCache_File';
181
182
        $this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : ''));
183
        $this->cacheKey = 'manifest';
184
185
        if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) {
186
            $this->classes      = $data['classes'];
187
            $this->descendants  = $data['descendants'];
188
            $this->interfaces   = $data['interfaces'];
189
            $this->implementors = $data['implementors'];
190
            $this->configs      = $data['configs'];
191
            $this->configDirs   = $data['configDirs'];
192
            $this->traits       = $data['traits'];
193
        } else {
194
            $this->regenerate($cache);
195
        }
196
    }
197
198
    /**
199
     * Returns the file path to a class or interface if it exists in the
200
     * manifest.
201
     *
202
     * @param  string $name
203
     * @return string|null
204
     */
205
    public function getItemPath($name)
206
    {
207
        $name = strtolower($name);
208
209
        foreach ([
210
            $this->classes,
211
            $this->interfaces,
212
            $this->traits
213
        ] as $source) {
214
            if (isset($source[$name]) && file_exists($source[$name])) {
215
                return $source[$name];
216
            }
217
        }
218
        return null;
219
    }
220
221
    /**
222
     * Returns a map of lowercased class names to file paths.
223
     *
224
     * @return array
225
     */
226
    public function getClasses()
227
    {
228
        return $this->classes;
229
    }
230
231
    /**
232
     * Returns a lowercase array of all the class names in the manifest.
233
     *
234
     * @return array
235
     */
236
    public function getClassNames()
237
    {
238
        return array_keys($this->classes);
239
    }
240
241
    /**
242
     * Returns a lowercase array of all trait names in the manifest
243
     *
244
     * @return array
245
     */
246
    public function getTraitNames()
247
    {
248
        return array_keys($this->traits);
249
    }
250
251
    /**
252
     * Returns an array of all the descendant data.
253
     *
254
     * @return array
255
     */
256
    public function getDescendants()
257
    {
258
        return $this->descendants;
259
    }
260
261
    /**
262
     * Returns an array containing all the descendants (direct and indirect)
263
     * of a class.
264
     *
265
     * @param  string|object $class
266
     * @return array
267
     */
268
    public function getDescendantsOf($class)
269
    {
270
        if (is_object($class)) {
271
            $class = get_class($class);
272
        }
273
274
        $lClass = strtolower($class);
275
276
        if (array_key_exists($lClass, $this->descendants)) {
277
            return $this->descendants[$lClass];
278
        } else {
279
            return array();
280
        }
281
    }
282
283
    /**
284
     * Returns a map of lowercased interface names to file locations.
285
     *
286
     * @return array
287
     */
288
    public function getInterfaces()
289
    {
290
        return $this->interfaces;
291
    }
292
293
    /**
294
     * Returns a map of lowercased interface names to the classes the implement
295
     * them.
296
     *
297
     * @return array
298
     */
299
    public function getImplementors()
300
    {
301
        return $this->implementors;
302
    }
303
304
    /**
305
     * Returns an array containing the class names that implement a certain
306
     * interface.
307
     *
308
     * @param  string $interface
309
     * @return array
310
     */
311
    public function getImplementorsOf($interface)
312
    {
313
        $interface = strtolower($interface);
314
315
        if (array_key_exists($interface, $this->implementors)) {
316
            return $this->implementors[$interface];
317
        } else {
318
            return array();
319
        }
320
    }
321
322
    /**
323
     * Returns an array of paths to module config files.
324
     *
325
     * @return array
326
     */
327
    public function getConfigs()
328
    {
329
        return $this->configs;
330
    }
331
332
    /**
333
     * Returns an array of module names mapped to their paths.
334
     *
335
     * "Modules" in SilverStripe are simply directories with a _config.php
336
     * file.
337
     *
338
     * @return array
339
     */
340
    public function getModules()
341
    {
342
        $modules = array();
343
344
        if ($this->configs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->configs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
345
            foreach ($this->configs as $configPath) {
346
                $modules[basename(dirname($configPath))] = dirname($configPath);
347
            }
348
        }
349
350
        if ($this->configDirs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->configDirs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
351
            foreach ($this->configDirs as $configDir) {
352
                $path = preg_replace('/\/_config$/', '', dirname($configDir));
353
                $modules[basename($path)] = $path;
354
            }
355
        }
356
357
        return $modules;
358
    }
359
360
    /**
361
     * Get module that owns this class
362
     *
363
     * @param string $class Class name
364
     * @return string
365
     */
366
    public function getOwnerModule($class)
367
    {
368
        $path = realpath($this->getItemPath($class));
369
        if (!$path) {
370
            return null;
371
        }
372
373
        // Find based on loaded modules
374
        foreach ($this->getModules() as $parent => $module) {
375
            if (stripos($path, realpath($parent)) === 0) {
376
                return $module;
377
            }
378
        }
379
380
        // Assume top level folder is the module name
381
        $relativePath = substr($path, strlen(realpath(Director::baseFolder())));
382
        $parts = explode('/', trim($relativePath, '/'));
383
        return array_shift($parts);
384
    }
385
386
    /**
387
     * Completely regenerates the manifest file.
388
     *
389
     * @param bool $cache Cache the result.
390
     */
391
    public function regenerate($cache = true)
392
    {
393
        $resets = array(
394
            'classes', 'roots', 'children', 'descendants', 'interfaces',
395
            'implementors', 'configs', 'configDirs', 'traits'
396
        );
397
398
        // Reset the manifest so stale info doesn't cause errors.
399
        foreach ($resets as $reset) {
400
            $this->$reset = array();
401
        }
402
403
        $finder = new ManifestFileFinder();
404
        $finder->setOptions(array(
405
            'name_regex'    => '/^((_config)|([^_].*))\\.php$/',
406
            'ignore_files'  => array('index.php', 'main.php', 'cli-script.php'),
407
            'ignore_tests'  => !$this->tests,
408
            'file_callback' => array($this, 'handleFile'),
409
            'dir_callback' => array($this, 'handleDir')
410
        ));
411
        $finder->find($this->base);
412
413
        foreach ($this->roots as $root) {
414
            $this->coalesceDescendants($root);
415
        }
416
417
        if ($cache) {
418
            $data = array(
419
                'classes'      => $this->classes,
420
                'descendants'  => $this->descendants,
421
                'interfaces'   => $this->interfaces,
422
                'implementors' => $this->implementors,
423
                'configs'      => $this->configs,
424
                'configDirs'   => $this->configDirs,
425
                'traits'       => $this->traits,
426
            );
427
            $this->cache->save($data, $this->cacheKey);
428
        }
429
    }
430
431
    public function handleDir($basename, $pathname, $depth)
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
432
    {
433
        if ($basename == self::CONF_DIR) {
434
            $this->configDirs[] = $pathname;
435
        }
436
    }
437
438
    /**
439
     * Find a the full namespaced declaration of a class (or interface) from a list of candidate imports
440
     *
441
     * This is typically used to determine the full class name in classes that have imported namesapced symbols (having
442
     * used the `use` keyword)
443
     *
444
     * NB: remember the '\\' is an escaped backslash and is interpreted as a single \
445
     *
446
     * @param string $class The class (or interface) name to find in the candidate imports
447
     * @param string $namespace The namespace that was declared for the classes definition (if there was one)
448
     * @param array $imports The list of imported symbols (Classes or Interfaces) to test against
449
     *
450
     * @return string The fully namespaced class name
451
     */
452
    protected function findClassOrInterfaceFromCandidateImports($class, $namespace = '', $imports = array())
453
    {
454
455
        //normalise the namespace
456
        $namespace = rtrim($namespace, '\\');
457
458
        //by default we'll use the $class as our candidate
459
        $candidateClass = $class;
460
461
        if (!$class) {
462
            return $candidateClass;
463
        }
464
        //if the class starts with a \ then it is explicitly in the global namespace and we don't need to do
465
        // anything else
466
        if (substr($class, 0, 1) == '\\') {
467
            $candidateClass = substr($class, 1);
468
            return $candidateClass;
469
        }
470
        //if there's a namespace, starting assumption is the class is defined in that namespace
471
        if ($namespace) {
472
            $candidateClass = $namespace . '\\' . $class;
473
        }
474
475
        if (empty($imports)) {
476
            return $candidateClass;
477
        }
478
479
        //normalised class name (PHP is case insensitive for symbols/namespaces
480
        $lClass = strtolower($class);
481
482
        //go through all the imports and see if the class exists within one of them
483
        foreach ($imports as $alias => $import) {
484
            //normalise import
485
            $import = trim($import, '\\');
486
487
            //if there is no string key, then there was no declared alias - we'll use the main declaration
488
            if (is_int($alias)) {
489
                $alias = strtolower($import);
490
            } else {
491
                $alias = strtolower($alias);
492
            }
493
494
            //exact match? Then it's a class in the global namespace that was imported OR it's an alias of
495
            // another namespace
496
            // or if it ends with the \ClassName then it's the class we are looking for
497
            if ($lClass == $alias
498
                || substr_compare(
499
                    $alias,
500
                    '\\' . $lClass,
501
                    strlen($alias) - strlen($lClass) - 1,
502
                    // -1 because the $lClass length is 1 longer due to \
503
                    strlen($alias)
504
                ) === 0
505
            ) {
506
                $candidateClass = $import;
507
                break;
508
            }
509
        }
510
        return $candidateClass;
511
    }
512
513
    /**
514
     * Return an array of array($alias => $import) from tokenizer's tokens of a PHP file
515
     *
516
     * NB: If there is no alias we don't set a key to the array
517
     *
518
     * @param array $tokens The parsed tokens from tokenizer's parsing of a PHP file
519
     *
520
     * @return array The array of imports as (optional) $alias => $import
521
     */
522
    protected function getImportsFromTokens($tokens)
523
    {
524
        //parse out the imports
525
        $imports = self::get_imported_namespace_parser()->findAll($tokens);
526
527
        //if there are any imports, clean them up
528
        // imports come to us as array('importString' => array([array of matching tokens]))
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
529
        // we need to join this nested array into a string and split out the alias and the import
530
        if (!empty($imports)) {
531
            $cleanImports = array();
532
            foreach ($imports as $import) {
533
                if (!empty($import['importString'])) {
534
                    //join the array up into a string
535
                    $importString = implode('', $import['importString']);
536
                    //split at , to get each import declaration
537
                    $importSet = explode(',', $importString);
538
                    foreach ($importSet as $importDeclaration) {
539
                        //split at ' as ' (any case) to see if we are aliasing the namespace
540
                        $importDeclaration = preg_split('/\s+as\s+/i', $importDeclaration);
541
                        //shift off the fully namespaced import
542
                        $qualifiedImport = array_shift($importDeclaration);
543
                        //if there are still items in the array, it's the alias
544
                        if (!empty($importDeclaration)) {
545
                            $cleanImports[array_shift($importDeclaration)] = $qualifiedImport;
546
                        } else {
547
                            $cleanImports[] = $qualifiedImport;
548
                        }
549
                    }
550
                }
551
            }
552
            $imports = $cleanImports;
553
        }
554
        return $imports;
555
    }
556
557
    public function handleFile($basename, $pathname, $depth)
0 ignored issues
show
Unused Code introduced by
The parameter $depth is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
558
    {
559
        if ($basename == self::CONF_FILE) {
560
            $this->configs[] = $pathname;
561
            return;
562
        }
563
564
        $classes    = null;
565
        $interfaces = null;
566
        $namespace = null;
567
        $imports = null;
568
        $traits = null;
569
570
        // The results of individual file parses are cached, since only a few
571
        // files will have changed and TokenisedRegularExpression is quite
572
        // slow. A combination of the file name and file contents hash are used,
573
        // since just using the datetime lead to problems with upgrading.
574
        $key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname);
575
576
        $valid = false;
577
        if ($data = $this->cache->load($key)) {
578
            $valid = (
579
                isset($data['classes']) && is_array($data['classes'])
580
                && isset($data['interfaces']) && is_array($data['interfaces'])
581
                && isset($data['namespace']) && is_string($data['namespace'])
582
                && isset($data['imports']) && is_array($data['imports'])
583
                && isset($data['traits']) && is_array($data['traits'])
584
            );
585
586
            if ($valid) {
587
                $classes = $data['classes'];
588
                $interfaces = $data['interfaces'];
589
                $namespace = $data['namespace'];
590
                $imports = $data['imports'];
591
                $traits = $data['traits'];
592
            }
593
        }
594
595
        if (!$valid) {
596
            $tokens = token_get_all(file_get_contents($pathname));
597
598
            $classes = self::get_namespaced_class_parser()->findAll($tokens);
599
            $traits = self::get_trait_parser()->findAll($tokens);
600
601
            $namespace = self::get_namespace_parser()->findAll($tokens);
602
603
            if ($namespace) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $namespace of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
604
                $namespace = implode('', $namespace[0]['namespaceName']);
605
            } else {
606
                $namespace = '';
607
            }
608
609
            $imports = $this->getImportsFromTokens($tokens);
610
611
            $interfaces = self::get_interface_parser()->findAll($tokens);
612
613
            $cache = array(
614
                'classes' => $classes,
615
                'interfaces' => $interfaces,
616
                'namespace' => $namespace,
617
                'imports' => $imports,
618
                'traits' => $traits
619
            );
620
            $this->cache->save($cache, $key);
621
        }
622
623
        // Ensure namespace has no trailing slash, and namespaceBase does
624
        $namespaceBase = '';
625
        if ($namespace) {
626
            $namespace = rtrim($namespace, '\\');
627
            $namespaceBase = $namespace . '\\';
628
        }
629
630
        foreach ($classes as $class) {
631
            $name = $namespaceBase . $class['className'];
632
            $extends = isset($class['extends']) ? implode('', $class['extends']) : null;
633
            $implements = isset($class['interfaces']) ? $class['interfaces'] : null;
634
635
            if ($extends) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extends of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
636
                $extends = $this->findClassOrInterfaceFromCandidateImports($extends, $namespace, $imports);
637
            }
638
639
            if (!empty($implements)) {
640
                //join all the tokens
641
                $implements = implode('', $implements);
642
                //split at comma
643
                $implements = explode(',', $implements);
644
                //normalise interfaces
645
                foreach ($implements as &$interface) {
646
                    $interface = $this->findClassOrInterfaceFromCandidateImports($interface, $namespace, $imports);
647
                }
648
                //release the var name
649
                unset($interface);
650
            }
651
652
            $lowercaseName = strtolower($name);
653
            if (array_key_exists($lowercaseName, $this->classes)) {
654
                throw new Exception(sprintf(
655
                    'There are two files containing the "%s" class: "%s" and "%s"',
656
                    $name,
657
                    $this->classes[$lowercaseName],
658
                    $pathname
659
                ));
660
            }
661
662
            $this->classes[$lowercaseName] = $pathname;
663
664
            if ($extends) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extends of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
665
                $extends = strtolower($extends);
666
667
                if (!isset($this->children[$extends])) {
668
                    $this->children[$extends] = array($name);
669
                } else {
670
                    $this->children[$extends][] = $name;
671
                }
672
            } else {
673
                $this->roots[] = $name;
674
            }
675
676
            if ($implements) {
677
                foreach ($implements as $interface) {
678
                    $interface = strtolower($interface);
679
680
                    if (!isset($this->implementors[$interface])) {
681
                        $this->implementors[$interface] = array($name);
682
                    } else {
683
                        $this->implementors[$interface][] = $name;
684
                    }
685
                }
686
            }
687
        }
688
689
        foreach ($interfaces as $interface) {
690
            $this->interfaces[strtolower($namespaceBase . $interface['interfaceName'])] = $pathname;
691
        }
692
        foreach ($traits as $trait) {
693
            $this->traits[strtolower($namespaceBase . $trait['traitName'])] = $pathname;
694
        }
695
    }
696
697
    /**
698
     * Recursively coalesces direct child information into full descendant
699
     * information.
700
     *
701
     * @param  string $class
702
     * @return array
703
     */
704
    protected function coalesceDescendants($class)
705
    {
706
        $lClass = strtolower($class);
707
708
        if (array_key_exists($lClass, $this->children)) {
709
            $this->descendants[$lClass] = array();
710
711
            foreach ($this->children[$lClass] as $class) {
712
                $this->descendants[$lClass] = array_merge(
713
                    $this->descendants[$lClass],
714
                    array($class),
715
                    $this->coalesceDescendants($class)
716
                );
717
            }
718
719
            return $this->descendants[$lClass];
720
        } else {
721
            return array();
722
        }
723
    }
724
}
725