Completed
Push — master ( f5d71d...bfd9cb )
by Sam
12:52
created

ClassManifest::getDescendantsOf()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 13
rs 9.4285
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use Exception;
6
7
/**
8
 * A utility class which builds a manifest of all classes, interfaces and some
9
 * additional items present in a directory, and caches it.
10
 *
11
 * It finds the following information:
12
 *   - Class and interface names and paths.
13
 *   - All direct and indirect descendants of a class.
14
 *   - All implementors of an interface.
15
 *   - All module configuration files.
16
 */
17
class ClassManifest {
18
19
	const CONF_FILE = '_config.php';
20
	const CONF_DIR = '_config';
21
22
	protected $base;
23
	protected $tests;
24
25
	/**
26
	 * @var ManifestCache
27
	 */
28
	protected $cache;
29
30
	/**
31
	 * @var string
32
	 */
33
	protected $cacheKey;
34
35
	protected $classes      = array();
36
	protected $roots        = array();
37
	protected $children     = array();
38
	protected $descendants  = array();
39
	protected $interfaces   = array();
40
	protected $implementors = array();
41
	protected $configs      = array();
42
	protected $configDirs   = array();
43
	protected $traits		= array();
44
45
	/**
46
	 * @return TokenisedRegularExpression
47
	 */
48
	public static function get_class_parser() {
49
		return new TokenisedRegularExpression(array(
50
			0 => T_CLASS,
51
			1 => T_WHITESPACE,
52
			2 => array(T_STRING, 'can_jump_to' => array(7, 14), 'save_to' => 'className'),
53
			3 => T_WHITESPACE,
54
			4 => T_EXTENDS,
55
			5 => T_WHITESPACE,
56
			6 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => 14),
57
			7 => T_WHITESPACE,
58
			8 => T_IMPLEMENTS,
59
			9 => T_WHITESPACE,
60
			10 => array(T_STRING, 'can_jump_to' => 14, 'save_to' => 'interfaces[]'),
61
			11 => array(T_WHITESPACE, 'optional' => true),
62
			12 => array(',', 'can_jump_to' => 10, 'save_to' => 'interfaces[]'),
63
			13 => array(T_WHITESPACE, 'can_jump_to' => 10),
64
			14 => array(T_WHITESPACE, 'optional' => true),
65
			15 => '{',
66
		));
67
	}
68
69
	/**
70
	 * @return TokenisedRegularExpression
71
	 */
72
	public static function get_namespaced_class_parser() {
73
		return new TokenisedRegularExpression(array(
74
			0 => T_CLASS,
75
			1 => T_WHITESPACE,
76
			2 => array(T_STRING, 'can_jump_to' => array(8, 16), 'save_to' => 'className'),
77
			3 => T_WHITESPACE,
78
			4 => T_EXTENDS,
79
			5 => T_WHITESPACE,
80
			6 => array(T_NS_SEPARATOR, 'save_to' => 'extends[]', 'optional' => true),
81
			7 => array(T_STRING, 'save_to' => 'extends[]', 'can_jump_to' => array(6, 16)),
82
			8 => T_WHITESPACE,
83
			9 => T_IMPLEMENTS,
84
			10 => T_WHITESPACE,
85
			11 => array(T_NS_SEPARATOR, 'save_to' => 'interfaces[]', 'optional' => true),
86
			12 => array(T_STRING, 'can_jump_to' => array(11, 16), 'save_to' => 'interfaces[]'),
87
			13 => array(T_WHITESPACE, 'optional' => true),
88
			14 => array(',', 'can_jump_to' => 11, 'save_to' => 'interfaces[]'),
89
			15 => array(T_WHITESPACE, 'can_jump_to' => 11),
90
			16 => array(T_WHITESPACE, 'optional' => true),
91
			17 => '{',
92
		));
93
	}
94
95
	/**
96
	 * @return TokenisedRegularExpression
97
	 */
98
	public static function get_trait_parser() {
99
		return new TokenisedRegularExpression(array(
100
			0 => T_TRAIT,
101
			1 => T_WHITESPACE,
102
			2 => array(T_STRING, 'save_to' => 'traitName')
103
		));
104
	}
105
106
	/**
107
	 * @return TokenisedRegularExpression
108
	 */
109
	public static function get_namespace_parser() {
110
		return new TokenisedRegularExpression(array(
111
			0 => T_NAMESPACE,
112
			1 => T_WHITESPACE,
113
			2 => array(T_NS_SEPARATOR, 'save_to' => 'namespaceName[]', 'optional' => true),
114
			3 => array(T_STRING, 'save_to' => 'namespaceName[]', 'can_jump_to' => 2),
115
			4 => array(T_WHITESPACE, 'optional' => true),
116
			5 => ';',
117
		));
118
	}
119
120
	/**
121
	 * @return TokenisedRegularExpression
122
	 */
123
	public static function get_interface_parser() {
124
		return new TokenisedRegularExpression(array(
125
			0 => T_INTERFACE,
126
			1 => T_WHITESPACE,
127
			2 => array(T_STRING, 'save_to' => 'interfaceName')
128
		));
129
	}
130
131
	/**
132
	 * Create a {@link TokenisedRegularExpression} that extracts the namespaces imported with the 'use' keyword
133
	 *
134
	 * This searches symbols for a `use` followed by 1 or more namespaces which are optionally aliased using the `as`
135
	 * keyword. The relevant matching tokens are added one-by-one into an array (using `save_to` param).
136
	 *
137
	 * eg: use Namespace\ClassName as Alias, OtherNamespace\ClassName;
138
	 *
139
	 * @return TokenisedRegularExpression
140
	 */
141
	public static function get_imported_namespace_parser() {
142
		return new TokenisedRegularExpression(array(
143
			0 => T_USE,
144
			1 => T_WHITESPACE,
145
			2 => array(T_NS_SEPARATOR, 'save_to' => 'importString[]', 'optional' => true),
146
			3 => array(T_STRING, 'save_to' => 'importString[]', 'can_jump_to' => array(2, 8)),
147
			4 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
148
			5 => array(T_AS, 'save_to' => 'importString[]'),
149
			6 => array(T_WHITESPACE, 'save_to' => 'importString[]'),
150
			7 => array(T_STRING, 'save_to' => 'importString[]'),
151
			8 => array(T_WHITESPACE, 'optional' => true),
152
			9 => array(',', 'save_to' => 'importString[]', 'optional' => true, 'can_jump_to' => 2),
153
			10 => array(T_WHITESPACE, 'optional' => true, 'can_jump_to' => 2),
154
			11 => ';',
155
		));
156
	}
157
158
	/**
159
	 * Constructs and initialises a new class manifest, either loading the data
160
	 * from the cache or re-scanning for classes.
161
	 *
162
	 * @param string $base The manifest base path.
163
	 * @param bool   $includeTests Include the contents of "tests" directories.
164
	 * @param bool   $forceRegen Force the manifest to be regenerated.
165
	 * @param bool   $cache If the manifest is regenerated, cache it.
166
	 */
167
	public function __construct($base, $includeTests = false, $forceRegen = false, $cache = true) {
168
		$this->base  = $base;
169
		$this->tests = $includeTests;
170
171
		$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'SilverStripe\\Core\\Manifest\\ManifestCache_File';
172
173
		$this->cache = new $cacheClass('classmanifest'.($includeTests ? '_tests' : ''));
174
		$this->cacheKey = 'manifest';
175
176
		if (!$forceRegen && $data = $this->cache->load($this->cacheKey)) {
177
			$this->classes      = $data['classes'];
178
			$this->descendants  = $data['descendants'];
179
			$this->interfaces   = $data['interfaces'];
180
			$this->implementors = $data['implementors'];
181
			$this->configs      = $data['configs'];
182
			$this->configDirs   = $data['configDirs'];
183
			$this->traits		= $data['traits'];
184
		} else {
185
			$this->regenerate($cache);
186
		}
187
	}
188
189
	/**
190
	 * Returns the file path to a class or interface if it exists in the
191
	 * manifest.
192
	 *
193
	 * @param  string $name
194
	 * @return string|null
195
	 */
196
	public function getItemPath($name) {
197
		$name = strtolower($name);
198
199
		if (isset($this->classes[$name])) {
200
			return $this->classes[$name];
201
		} elseif (isset($this->interfaces[$name])) {
202
			return $this->interfaces[$name];
203
		} elseif(isset($this->traits[$name])) {
204
			return $this->traits[$name];
205
		}
206
		return null;
207
	}
208
209
	/**
210
	 * Returns a map of lowercased class names to file paths.
211
	 *
212
	 * @return array
213
	 */
214
	public function getClasses() {
215
		return $this->classes;
216
	}
217
218
	/**
219
	 * Returns a lowercase array of all the class names in the manifest.
220
	 *
221
	 * @return array
222
	 */
223
	public function getClassNames() {
224
		return array_keys($this->classes);
225
	}
226
227
	/**
228
	 * Returns a lowercase array of all trait names in the manifest
229
	 *
230
	 * @return array
231
	 */
232
	public function getTraitNames() {
233
		return array_keys($this->traits);
234
	}
235
236
	/**
237
	 * Returns an array of all the descendant data.
238
	 *
239
	 * @return array
240
	 */
241
	public function getDescendants() {
242
		return $this->descendants;
243
	}
244
245
	/**
246
	 * Returns an array containing all the descendants (direct and indirect)
247
	 * of a class.
248
	 *
249
	 * @param  string|object $class
250
	 * @return array
251
	 */
252
	public function getDescendantsOf($class) {
253
		if (is_object($class)) {
254
			$class = get_class($class);
255
		}
256
257
		$lClass = strtolower($class);
258
259
		if (array_key_exists($lClass, $this->descendants)) {
260
			return $this->descendants[$lClass];
261
		} else {
262
			return array();
263
		}
264
	}
265
266
	/**
267
	 * Returns a map of lowercased interface names to file locations.
268
	 *
269
	 * @return array
270
	 */
271
	public function getInterfaces() {
272
		return $this->interfaces;
273
	}
274
275
	/**
276
	 * Returns a map of lowercased interface names to the classes the implement
277
	 * them.
278
	 *
279
	 * @return array
280
	 */
281
	public function getImplementors() {
282
		return $this->implementors;
283
	}
284
285
	/**
286
	 * Returns an array containing the class names that implement a certain
287
	 * interface.
288
	 *
289
	 * @param  string $interface
290
	 * @return array
291
	 */
292
	public function getImplementorsOf($interface) {
293
		$interface = strtolower($interface);
294
295
		if (array_key_exists($interface, $this->implementors)) {
296
			return $this->implementors[$interface];
297
		} else {
298
			return array();
299
		}
300
	}
301
302
	/**
303
	 * Returns an array of paths to module config files.
304
	 *
305
	 * @return array
306
	 */
307
	public function getConfigs() {
308
		return $this->configs;
309
	}
310
311
	/**
312
	 * Returns an array of module names mapped to their paths.
313
	 *
314
	 * "Modules" in SilverStripe are simply directories with a _config.php
315
	 * file.
316
	 *
317
	 * @return array
318
	 */
319
	public function getModules() {
320
		$modules = array();
321
322
		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...
323
			foreach($this->configs as $configPath) {
324
				$modules[basename(dirname($configPath))] = dirname($configPath);
325
			}
326
		}
327
328
		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...
329
			foreach($this->configDirs as $configDir) {
330
				$path = preg_replace('/\/_config$/', '', dirname($configDir));
331
				$modules[basename($path)] = $path;
332
			}
333
		}
334
335
		return $modules;
336
	}
337
338
	/**
339
	 * Used to set up files that we want to exclude from parsing for performance reasons.
340
	 */
341
	protected function setDefaults()
342
	{
343
		$this->classes['sstemplateparser'] = FRAMEWORK_PATH.'/View/SSTemplateParser.php';
344
		$this->classes['sstemplateparseexception'] = FRAMEWORK_PATH.'/View/SSTemplateParseException.php';
345
	}
346
347
	/**
348
	 * Completely regenerates the manifest file.
349
	 *
350
	 * @param bool $cache Cache the result.
351
	 */
352
	public function regenerate($cache = true) {
353
		$resets = array(
354
			'classes', 'roots', 'children', 'descendants', 'interfaces',
355
			'implementors', 'configs', 'configDirs', 'traits'
356
		);
357
358
		// Reset the manifest so stale info doesn't cause errors.
359
		foreach ($resets as $reset) {
360
			$this->$reset = array();
361
		}
362
363
		$this->setDefaults();
364
365
		$finder = new ManifestFileFinder();
366
		$finder->setOptions(array(
367
			'name_regex'    => '/^(_config.php|[^_].*\.php)$/',
368
			'ignore_files'  => array('index.php', 'main.php', 'cli-script.php', 'SSTemplateParser.php'),
369
			'ignore_tests'  => !$this->tests,
370
			'file_callback' => array($this, 'handleFile'),
371
			'dir_callback' => array($this, 'handleDir')
372
		));
373
		$finder->find($this->base);
374
375
		foreach ($this->roots as $root) {
376
			$this->coalesceDescendants($root);
377
		}
378
379
		if ($cache) {
380
			$data = array(
381
				'classes'      => $this->classes,
382
				'descendants'  => $this->descendants,
383
				'interfaces'   => $this->interfaces,
384
				'implementors' => $this->implementors,
385
				'configs'      => $this->configs,
386
				'configDirs'   => $this->configDirs,
387
				'traits'       => $this->traits,
388
			);
389
			$this->cache->save($data, $this->cacheKey);
390
		}
391
	}
392
393
	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...
394
		if ($basename == self::CONF_DIR) {
395
			$this->configDirs[] = $pathname;
396
		}
397
	}
398
399
	/**
400
	 * Find a the full namespaced declaration of a class (or interface) from a list of candidate imports
401
	 *
402
	 * This is typically used to determine the full class name in classes that have imported namesapced symbols (having
403
	 * used the `use` keyword)
404
	 *
405
	 * NB: remember the '\\' is an escaped backslash and is interpreted as a single \
406
	 *
407
	 * @param string $class The class (or interface) name to find in the candidate imports
408
	 * @param string $namespace The namespace that was declared for the classes definition (if there was one)
409
	 * @param array $imports The list of imported symbols (Classes or Interfaces) to test against
410
	 *
411
	 * @return string The fully namespaced class name
412
	 */
413
	protected function findClassOrInterfaceFromCandidateImports($class, $namespace = '', $imports = array()) {
414
415
		//normalise the namespace
416
		$namespace = rtrim($namespace, '\\');
417
418
		//by default we'll use the $class as our candidate
419
		$candidateClass = $class;
420
421
		if (!$class) {
422
			return $candidateClass;
423
		}
424
		//if the class starts with a \ then it is explicitly in the global namespace and we don't need to do
425
		// anything else
426
		if (substr($class, 0, 1) == '\\') {
427
			$candidateClass = substr($class, 1);
428
			return $candidateClass;
429
		}
430
		//if there's a namespace, starting assumption is the class is defined in that namespace
431
		if ($namespace) {
432
			$candidateClass = $namespace . '\\' . $class;
433
		}
434
435
		if (empty($imports)) {
436
			return $candidateClass;
437
		}
438
439
		//normalised class name (PHP is case insensitive for symbols/namespaces
440
		$lClass = strtolower($class);
441
442
		//go through all the imports and see if the class exists within one of them
443
		foreach ($imports as $alias => $import) {
444
			//normalise import
445
			$import = trim($import, '\\');
446
447
			//if there is no string key, then there was no declared alias - we'll use the main declaration
448
			if (is_int($alias)) {
449
				$alias = strtolower($import);
450
			} else {
451
				$alias = strtolower($alias);
452
			}
453
454
			//exact match? Then it's a class in the global namespace that was imported OR it's an alias of
455
			// another namespace
456
			// or if it ends with the \ClassName then it's the class we are looking for
457
			if ($lClass == $alias
458
				|| substr_compare(
459
					$alias,
460
					'\\' . $lClass,
461
					strlen($alias) - strlen($lClass) - 1,
462
					// -1 because the $lClass length is 1 longer due to \
463
					strlen($alias)
464
				) === 0
465
			) {
466
				$candidateClass = $import;
467
				break;
468
			}
469
		}
470
		return $candidateClass;
471
	}
472
473
	/**
474
	 * Return an array of array($alias => $import) from tokenizer's tokens of a PHP file
475
	 *
476
	 * NB: If there is no alias we don't set a key to the array
477
	 *
478
	 * @param array $tokens The parsed tokens from tokenizer's parsing of a PHP file
479
	 *
480
	 * @return array The array of imports as (optional) $alias => $import
481
	 */
482
	protected function getImportsFromTokens($tokens) {
483
		//parse out the imports
484
		$imports = self::get_imported_namespace_parser()->findAll($tokens);
485
486
		//if there are any imports, clean them up
487
		// 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...
488
		// we need to join this nested array into a string and split out the alias and the import
489
		if (!empty($imports)) {
490
			$cleanImports = array();
491
			foreach ($imports as $import) {
492
				if (!empty($import['importString'])) {
493
					//join the array up into a string
494
					$importString = implode('', $import['importString']);
495
					//split at , to get each import declaration
496
					$importSet = explode(',', $importString);
497
					foreach ($importSet as $importDeclaration) {
498
						//split at ' as ' (any case) to see if we are aliasing the namespace
499
						$importDeclaration = preg_split('/\s+as\s+/i', $importDeclaration);
500
						//shift off the fully namespaced import
501
						$qualifiedImport = array_shift($importDeclaration);
502
						//if there are still items in the array, it's the alias
503
						if (!empty($importDeclaration)) {
504
							$cleanImports[array_shift($importDeclaration)] = $qualifiedImport;
505
						}
506
						else {
507
							$cleanImports[] = $qualifiedImport;
508
						}
509
					}
510
				}
511
			}
512
			$imports = $cleanImports;
513
		}
514
		return $imports;
515
	}
516
517
	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...
518
		if ($basename == self::CONF_FILE) {
519
			$this->configs[] = $pathname;
520
			return;
521
		}
522
523
		$classes    = null;
524
		$interfaces = null;
525
		$namespace = null;
526
		$imports = null;
527
		$traits = null;
528
529
		// The results of individual file parses are cached, since only a few
530
		// files will have changed and TokenisedRegularExpression is quite
531
		// slow. A combination of the file name and file contents hash are used,
532
		// since just using the datetime lead to problems with upgrading.
533
		$key = preg_replace('/[^a-zA-Z0-9_]/', '_', $basename) . '_' . md5_file($pathname);
534
535
		$valid = false;
536
		if ($data = $this->cache->load($key)) {
537
			$valid = (
538
				isset($data['classes']) && is_array($data['classes'])
539
				&& isset($data['interfaces']) && is_array($data['interfaces'])
540
				&& isset($data['namespace']) && is_string($data['namespace'])
541
				&& isset($data['imports']) && is_array($data['imports'])
542
				&& isset($data['traits']) && is_array($data['traits'])
543
			);
544
545
			if ($valid) {
546
				$classes = $data['classes'];
547
				$interfaces = $data['interfaces'];
548
				$namespace = $data['namespace'];
549
				$imports = $data['imports'];
550
				$traits = $data['traits'];
551
			}
552
		}
553
554
		if (!$valid) {
555
			$tokens = token_get_all(file_get_contents($pathname));
556
557
			$classes = self::get_namespaced_class_parser()->findAll($tokens);
558
			$traits = self::get_trait_parser()->findAll($tokens);
559
560
			$namespace = self::get_namespace_parser()->findAll($tokens);
561
562
			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...
563
				$namespace = implode('', $namespace[0]['namespaceName']);
564
			} else {
565
				$namespace = '';
566
			}
567
568
			$imports = $this->getImportsFromTokens($tokens);
569
570
			$interfaces = self::get_interface_parser()->findAll($tokens);
571
572
			$cache = array(
573
				'classes' => $classes,
574
				'interfaces' => $interfaces,
575
				'namespace' => $namespace,
576
				'imports' => $imports,
577
				'traits' => $traits
578
			);
579
			$this->cache->save($cache, $key);
580
		}
581
582
		// Ensure namespace has no trailing slash, and namespaceBase does
583
		$namespaceBase = '';
584
		if ($namespace) {
585
			$namespace = rtrim($namespace, '\\');
586
			$namespaceBase = $namespace . '\\';
587
		}
588
589
		foreach ($classes as $class) {
590
			$name = $namespaceBase . $class['className'];
591
			$extends = isset($class['extends']) ? implode('', $class['extends']) : null;
592
			$implements = isset($class['interfaces']) ? $class['interfaces'] : null;
593
594
			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...
595
				$extends = $this->findClassOrInterfaceFromCandidateImports($extends, $namespace, $imports);
596
			}
597
598
			if (!empty($implements)) {
599
				//join all the tokens
600
				$implements = implode('', $implements);
601
				//split at comma
602
				$implements = explode(',', $implements);
603
				//normalise interfaces
604
				foreach ($implements as &$interface) {
605
					$interface = $this->findClassOrInterfaceFromCandidateImports($interface, $namespace, $imports);
606
				}
607
				//release the var name
608
				unset($interface);
609
			}
610
611
			$lowercaseName = strtolower($name);
612
			if (array_key_exists($lowercaseName, $this->classes)) {
613
				throw new Exception(sprintf(
614
					'There are two files containing the "%s" class: "%s" and "%s"',
615
					$name, $this->classes[$lowercaseName], $pathname
616
				));
617
			}
618
619
			$this->classes[$lowercaseName] = $pathname;
620
621
			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...
622
				$extends = strtolower($extends);
623
624
				if (!isset($this->children[$extends])) {
625
					$this->children[$extends] = array($name);
626
				} else {
627
					$this->children[$extends][] = $name;
628
				}
629
			} else {
630
				$this->roots[] = $name;
631
			}
632
633
			if ($implements) {
634
				foreach ($implements as $interface) {
635
					$interface = strtolower($interface);
636
637
					if (!isset($this->implementors[$interface])) {
638
						$this->implementors[$interface] = array($name);
639
					} else {
640
						$this->implementors[$interface][] = $name;
641
					}
642
				}
643
			}
644
		}
645
646
		foreach ($interfaces as $interface) {
647
			$this->interfaces[strtolower($namespaceBase . $interface['interfaceName'])] = $pathname;
648
		}
649
		foreach ($traits as $trait) {
650
			$this->traits[strtolower($namespaceBase . $trait['traitName'])] = $pathname;
651
		}
652
	}
653
654
	/**
655
	 * Recursively coalesces direct child information into full descendant
656
	 * information.
657
	 *
658
	 * @param  string $class
659
	 * @return array
660
	 */
661
	protected function coalesceDescendants($class) {
662
		$lClass = strtolower($class);
663
664
		if (array_key_exists($lClass, $this->children)) {
665
			$this->descendants[$lClass] = array();
666
667
			foreach ($this->children[$lClass] as $class) {
668
				$this->descendants[$lClass] = array_merge(
669
					$this->descendants[$lClass],
670
					array($class),
671
					$this->coalesceDescendants($class)
672
				);
673
			}
674
675
			return $this->descendants[$lClass];
676
		} else {
677
			return array();
678
		}
679
	}
680
681
}
682