ClassInfo   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 541
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 205
dl 0
loc 541
rs 2
c 0
b 0
f 0
wmc 82

19 Methods

Rating   Name   Duplication   Size   Complexity  
A allClasses() 0 3 1
A baseDataClass() 0 4 1
A reset_db_cache() 0 4 1
A hasTable() 0 7 3
A dataClassesFor() 0 16 3
A exists() 0 5 3
A getValidSubClasses() 0 14 4
A shortName() 0 5 1
A hasMethod() 0 9 4
A subclassesFor() 0 15 4
A classes_for_folder() 0 15 3
A has_method_from() 0 21 4
A implementorsOf() 0 3 1
B ancestry() 0 23 7
F parse_class_spec() 0 142 33
A table_for_object_field() 0 4 1
A class_name() 0 20 4
A classImplements() 0 5 1
A classes_for_file() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like ClassInfo 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.

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 ClassInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Core;
4
5
use Exception;
6
use ReflectionClass;
7
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use SilverStripe\Control\Director;
9
use SilverStripe\Core\Manifest\ClassLoader;
10
use SilverStripe\Dev\Deprecation;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DB;
13
14
/**
15
 * Provides introspection information about the class tree.
16
 *
17
 * It's a cached wrapper around the built-in class functions.  SilverStripe uses
18
 * class introspection heavily and without the caching it creates an unfortunate
19
 * performance hit.
20
 */
21
class ClassInfo
22
{
23
    /**
24
     * Cache for {@link hasTable()}
25
     *
26
     * @internal
27
     * @var array
28
     */
29
    private static $_cache_all_tables = array();
30
31
    /**
32
     * @internal
33
     * @var array Cache for {@link ancestry()}.
34
     */
35
    private static $_cache_ancestry = array();
36
37
    /**
38
     * Cache for parse_class_spec
39
     *
40
     * @internal
41
     * @var array
42
     */
43
    private static $_cache_parse = [];
44
45
    /**
46
     * Cache for has_method_from
47
     *
48
     * @internal
49
     * @var array
50
     */
51
    private static $_cache_methods = array();
52
53
    /**
54
     * Cache for class_name
55
     *
56
     * @internal
57
     * @var array
58
     */
59
    private static $_cache_class_names = [];
60
61
    /**
62
     * Wrapper for classes getter.
63
     *
64
     * @return array List of all class names
65
     */
66
    public static function allClasses()
67
    {
68
        return ClassLoader::inst()->getManifest()->getClassNames();
69
    }
70
71
    /**
72
     * Returns true if a class or interface name exists.
73
     *
74
     * @param string $class
75
     * @return bool
76
     */
77
    public static function exists($class)
78
    {
79
        return class_exists($class, false)
80
            || interface_exists($class, false)
81
            || ClassLoader::inst()->getItemPath($class);
82
    }
83
84
    /**
85
     * @todo Move this to SS_Database or DB
86
     *
87
     * @param string $tableName
88
     * @return bool
89
     */
90
    public static function hasTable($tableName)
91
    {
92
        // Cache the list of all table names to reduce on DB traffic
93
        if (empty(self::$_cache_all_tables) && DB::is_active()) {
94
            self::$_cache_all_tables = DB::get_schema()->tableList();
95
        }
96
        return !empty(self::$_cache_all_tables[strtolower($tableName)]);
97
    }
98
99
    public static function reset_db_cache()
100
    {
101
        self::$_cache_all_tables = null;
102
        self::$_cache_ancestry = array();
103
    }
104
105
    /**
106
     * Returns the manifest of all classes which are present in the database.
107
     *
108
     * @param string $class Class name to check enum values for ClassName field
109
     * @param boolean $includeUnbacked Flag indicating whether or not to include
110
     * types that don't exist as implemented classes. By default these are excluded.
111
     * @return array List of subclasses
112
     */
113
    public static function getValidSubClasses($class = SiteTree::class, $includeUnbacked = false)
114
    {
115
        if (is_string($class) && !class_exists($class)) {
116
            return array();
117
        }
118
119
        $class = self::class_name($class);
120
        if ($includeUnbacked) {
121
            $table = DataObject::getSchema()->tableName($class);
122
            $classes = DB::get_schema()->enumValuesForField($table, 'ClassName');
123
        } else {
124
            $classes = static::subclassesFor($class);
125
        }
126
        return $classes;
127
    }
128
129
    /**
130
     * Returns an array of the current class and all its ancestors and children
131
     * which require a DB table.
132
     *
133
     * @todo Move this into {@see DataObjectSchema}
134
     *
135
     * @param string|object $nameOrObject Class or object instance
136
     * @return array
137
     */
138
    public static function dataClassesFor($nameOrObject)
139
    {
140
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
141
            return [];
142
        }
143
144
        // Get all classes
145
        $class = self::class_name($nameOrObject);
146
        $classes = array_merge(
147
            self::ancestry($class),
148
            self::subclassesFor($class)
149
        );
150
151
        // Filter by table
152
        return array_filter($classes, function ($next) {
153
            return DataObject::getSchema()->classHasTable($next);
154
        });
155
    }
156
157
    /**
158
     * @deprecated 4.0.0:5.0.0
159
     * @param string $class
160
     * @return string
161
     */
162
    public static function baseDataClass($class)
163
    {
164
        Deprecation::notice('5.0', 'Use DataObject::getSchema()->baseDataClass()');
165
        return DataObject::getSchema()->baseDataClass($class);
166
    }
167
168
    /**
169
     * Returns a list of classes that inherit from the given class.
170
     * The resulting array includes the base class passed
171
     * through the $class parameter as the first array value.
172
     * Note that keys are lowercase, while the values are correct case.
173
     *
174
     * Example usage:
175
     * <code>
176
     * ClassInfo::subclassesFor('BaseClass');
177
     *  array(
178
     *  'baseclass' => 'BaseClass',
179
     *  'childclass' => 'ChildClass',
180
     *  'grandchildclass' => 'GrandChildClass'
181
     * )
182
     * </code>
183
     *
184
     * @param string|object $nameOrObject The classname or object
185
     * @param bool $includeBaseClass Whether to include the base class or not. Defaults to true.
186
     * @return array List of class names with lowercase keys and correct-case values
187
     * @throws \ReflectionException
188
     */
189
    public static function subclassesFor($nameOrObject, $includeBaseClass = true)
190
    {
191
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
192
            return [];
193
        }
194
195
        // Get class names
196
        $className = self::class_name($nameOrObject);
197
        $lowerClassName = strtolower($className);
198
199
        // Merge with descendants
200
        $descendants = ClassLoader::inst()->getManifest()->getDescendantsOf($className);
201
        return array_merge(
202
            $includeBaseClass ? [$lowerClassName => $className] : [],
203
            $descendants
204
        );
205
    }
206
207
    /**
208
     * Convert a class name in any case and return it as it was defined in PHP
209
     *
210
     * eg: self::class_name('dataobJEct'); //returns 'DataObject'
211
     *
212
     * @param string|object $nameOrObject The classname or object you want to normalise
213
     * @throws \ReflectionException
214
     * @return string The normalised class name
215
     */
216
    public static function class_name($nameOrObject)
217
    {
218
        if (is_object($nameOrObject)) {
219
            return get_class($nameOrObject);
220
        }
221
222
        $key = strtolower($nameOrObject);
223
        if (!isset(static::$_cache_class_names[$key])) {
0 ignored issues
show
Bug introduced by
Since $_cache_class_names is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $_cache_class_names to at least protected.
Loading history...
224
            // Get manifest name
225
            $name = ClassLoader::inst()->getManifest()->getItemName($nameOrObject);
226
227
            // Use reflection for non-manifest classes
228
            if (!$name) {
229
                $reflection = new ReflectionClass($nameOrObject);
230
                $name = $reflection->getName();
231
            }
232
            static::$_cache_class_names[$key] = $name;
233
        }
234
235
        return static::$_cache_class_names[$key];
236
    }
237
238
    /**
239
     * Returns the passed class name along with all its parent class names in an
240
     * array, sorted with the root class first.
241
     *
242
     * @param string|object $nameOrObject Class or object instance
243
     * @param bool $tablesOnly Only return classes that have a table in the db.
244
     * @return array List of class names with lowercase keys and correct-case values
245
     */
246
    public static function ancestry($nameOrObject, $tablesOnly = false)
247
    {
248
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
249
            return [];
250
        }
251
252
        $class = self::class_name($nameOrObject);
253
254
        $lowerClass = strtolower($class);
255
256
        $cacheKey = $lowerClass . '_' . (string)$tablesOnly;
257
        $parent = $class;
258
        if (!isset(self::$_cache_ancestry[$cacheKey])) {
259
            $ancestry = [];
260
            do {
261
                if (!$tablesOnly || DataObject::getSchema()->classHasTable($parent)) {
262
                    $ancestry[strtolower($parent)] = $parent;
263
                }
264
            } while ($parent = get_parent_class($parent));
265
            self::$_cache_ancestry[$cacheKey] = array_reverse($ancestry);
266
        }
267
268
        return self::$_cache_ancestry[$cacheKey];
269
    }
270
271
    /**
272
     * @param string $interfaceName
273
     * @return array A self-keyed array of class names with lowercase keys and correct-case values.
274
     * Note that this is only available with Silverstripe classes and not built-in PHP classes.
275
     */
276
    public static function implementorsOf($interfaceName)
277
    {
278
        return ClassLoader::inst()->getManifest()->getImplementorsOf($interfaceName);
279
    }
280
281
    /**
282
     * Returns true if the given class implements the given interface
283
     *
284
     * @param string $className
285
     * @param string $interfaceName
286
     * @return bool
287
     */
288
    public static function classImplements($className, $interfaceName)
289
    {
290
        $lowerClassName = strtolower($className);
291
        $implementors = self::implementorsOf($interfaceName);
292
        return isset($implementors[$lowerClassName]);
293
    }
294
295
    /**
296
     * Get all classes contained in a file.
297
     *
298
     * @param string $filePath Path to a PHP file (absolute or relative to webroot)
299
     * @return array Map of lowercase class names to correct class name
300
     */
301
    public static function classes_for_file($filePath)
302
    {
303
        $absFilePath = Director::getAbsFile($filePath);
304
        $classManifest = ClassLoader::inst()->getManifest();
305
        $classes = $classManifest->getClasses();
306
        $classNames = $classManifest->getClassNames();
307
308
        $matchedClasses = [];
309
        foreach ($classes as $lowerClass => $compareFilePath) {
310
            if (strcasecmp($absFilePath, $compareFilePath) === 0) {
311
                $matchedClasses[$lowerClass] = $classNames[$lowerClass];
312
            }
313
        }
314
315
        return $matchedClasses;
316
    }
317
318
    /**
319
     * Returns all classes contained in a certain folder.
320
     *
321
     * @param string $folderPath Relative or absolute folder path
322
     * @return array Map of lowercase class names to correct class name
323
     */
324
    public static function classes_for_folder($folderPath)
325
    {
326
        $absFolderPath = Director::getAbsFile($folderPath);
327
        $classManifest = ClassLoader::inst()->getManifest();
328
        $classes = $classManifest->getClasses();
329
        $classNames = $classManifest->getClassNames();
330
331
        $matchedClasses = [];
332
        foreach ($classes as $lowerClass => $compareFilePath) {
333
            if (stripos($compareFilePath, $absFolderPath) === 0) {
334
                $matchedClasses[$lowerClass] = $classNames[$lowerClass];
335
            }
336
        }
337
338
        return $matchedClasses;
339
    }
340
341
    /**
342
     * Determine if the given class method is implemented at the given comparison class
343
     *
344
     * @param string $class Class to get methods from
345
     * @param string $method Method name to lookup
346
     * @param string $compclass Parent class to test if this is the implementor
347
     * @return bool True if $class::$method is declared in $compclass
348
     */
349
    public static function has_method_from($class, $method, $compclass)
350
    {
351
        $lClass = strtolower($class);
352
        $lMethod = strtolower($method);
353
        $lCompclass = strtolower($compclass);
354
        if (!isset(self::$_cache_methods[$lClass])) {
355
            self::$_cache_methods[$lClass] = array();
356
        }
357
358
        if (!array_key_exists($lMethod, self::$_cache_methods[$lClass])) {
359
            self::$_cache_methods[$lClass][$lMethod] = false;
360
361
            $classRef = new ReflectionClass($class);
362
363
            if ($classRef->hasMethod($method)) {
364
                $methodRef = $classRef->getMethod($method);
365
                self::$_cache_methods[$lClass][$lMethod] = $methodRef->getDeclaringClass()->getName();
366
            }
367
        }
368
369
        return strtolower(self::$_cache_methods[$lClass][$lMethod]) === $lCompclass;
370
    }
371
372
    /**
373
     * @deprecated 4.0.0:5.0.0
374
     */
375
    public static function table_for_object_field($candidateClass, $fieldName)
376
    {
377
        Deprecation::notice('5.0', 'Use DataObject::getSchema()->tableForField()');
378
        return DataObject::getSchema()->tableForField($candidateClass, $fieldName);
379
    }
380
381
    /**
382
     * Strip namespace from class
383
     *
384
     * @param string|object $nameOrObject Name of class, or instance
385
     * @return string Name of class without namespace
386
     */
387
    public static function shortName($nameOrObject)
388
    {
389
        $name = static::class_name($nameOrObject);
390
        $parts = explode('\\', $name);
391
        return end($parts);
392
    }
393
394
    /**
395
     * Helper to determine if the given object has a method
396
     *
397
     * @param object $object
398
     * @param string $method
399
     * @return bool
400
     */
401
    public static function hasMethod($object, $method)
402
    {
403
        if (empty($object)) {
404
            return false;
405
        }
406
        if (method_exists($object, $method)) {
407
            return true;
408
        }
409
        return method_exists($object, 'hasMethod') && $object->hasMethod($method);
410
    }
411
412
    /**
413
     * Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
414
     * Returns a 2-element array, with classname and arguments
415
     *
416
     * @param string $classSpec
417
     * @return array
418
     * @throws Exception
419
     */
420
    public static function parse_class_spec($classSpec)
421
    {
422
        if (isset(static::$_cache_parse[$classSpec])) {
0 ignored issues
show
Bug introduced by
Since $_cache_parse is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $_cache_parse to at least protected.
Loading history...
423
            return static::$_cache_parse[$classSpec];
424
        }
425
426
        $tokens = token_get_all("<?php $classSpec");
427
        $class = null;
428
        $args = array();
429
430
        // Keep track of the current bucket that we're putting data into
431
        $bucket = &$args;
432
        $bucketStack = array();
433
        $hadNamespace = false;
434
        $currentKey = null;
435
436
        foreach ($tokens as $token) {
437
            // $forceResult used to allow null result to be detected
438
            $result = $forceResult = null;
439
            $tokenName = is_array($token) ? $token[0] : $token;
440
441
            // Get the class name
442
            if ($class === null && is_array($token) && $token[0] === T_STRING) {
443
                $class = $token[1];
444
            } elseif (is_array($token) && $token[0] === T_NS_SEPARATOR) {
445
                $class .= $token[1];
446
                $hadNamespace = true;
447
            } elseif ($token === '.') {
448
                // Treat service name separator as NS separator
449
                $class .= '.';
450
                $hadNamespace = true;
451
            } elseif ($hadNamespace && is_array($token) && $token[0] === T_STRING) {
452
                $class .= $token[1];
453
                $hadNamespace = false;
454
            // Get arguments
455
            } elseif (is_array($token)) {
456
                switch ($token[0]) {
457
                    case T_CONSTANT_ENCAPSED_STRING:
458
                        $argString = $token[1];
459
                        switch ($argString[0]) {
460
                            case '"':
461
                                $result = stripcslashes(substr($argString, 1, -1));
462
                                break;
463
                            case "'":
464
                                $result = str_replace(
465
                                    ["\\\\", "\\'"],
466
                                    ["\\", "'"],
467
                                    substr($argString, 1, -1)
468
                                );
469
                                break;
470
                            default:
471
                                throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
472
                        }
473
474
                        break;
475
476
                    case T_DNUMBER:
477
                        $result = (double)$token[1];
478
                        break;
479
480
                    case T_LNUMBER:
481
                        $result = (int)$token[1];
482
                        break;
483
484
                    case T_DOUBLE_ARROW:
485
                        // We've encountered an associative array (the array itself has already been
486
                        // added to the bucket), so the previous item added to the bucket is the key
487
                        end($bucket);
488
                        $currentKey = current($bucket);
489
                        array_pop($bucket);
490
                        break;
491
492
                    case T_STRING:
493
                        switch ($token[1]) {
494
                            case 'true':
495
                                $result = true;
496
497
                                break;
498
                            case 'false':
499
                                $result = false;
500
501
                                break;
502
                            case 'null':
503
                                $result = null;
504
                                $forceResult = true;
505
506
                                break;
507
                            default:
508
                                throw new Exception("Bad T_STRING arg '{$token[1]}'");
509
                        }
510
511
                        break;
512
513
                    case T_ARRAY:
514
                        $result = array();
515
                        break;
516
                }
517
            } else {
518
                if ($tokenName === '[') {
519
                    $result = array();
520
                } elseif (($tokenName === ')' || $tokenName === ']') && !empty($bucketStack)) {
521
                    // Store the bucket we're currently working on
522
                    $oldBucket = $bucket;
523
                    // Fetch the key for the bucket at the top of the stack
524
                    end($bucketStack);
525
                    $key = key($bucketStack);
526
                    reset($bucketStack);
527
                    // Re-instate the bucket from the top of the stack
528
                    $bucket = &$bucketStack[$key];
529
                    // Add our saved, "nested" bucket to the bucket we just popped off the stack
530
                    $bucket[$key] = $oldBucket;
531
                    // Remove the bucket we just popped off the stack
532
                    array_pop($bucketStack);
533
                }
534
            }
535
536
            // If we've got something to add to the bucket, add it
537
            if ($result !== null || $forceResult) {
538
                if ($currentKey) {
539
                    $bucket[$currentKey] = $result;
540
                    $currentKey = null;
541
                } else {
542
                    $bucket[] = $result;
543
                }
544
545
                // If we've just pushed an array, that becomes our new bucket
546
                if ($result === array()) {
547
                    // Fetch the key that the array was pushed to
548
                    end($bucket);
549
                    $key = key($bucket);
550
                    reset($bucket);
551
                    // Store reference to "old" bucket in the stack
552
                    $bucketStack[$key] = &$bucket;
553
                    // Set the active bucket to be our newly-pushed, empty array
554
                    $bucket = &$bucket[$key];
555
                }
556
            }
557
        }
558
559
        $result = [$class, $args];
560
        static::$_cache_parse[$classSpec] = $result;
561
        return $result;
562
    }
563
}
564