Completed
Pull Request — master (#6927)
by Damian
11:37 queued 03:26
created

ClassInfo   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 8

Importance

Changes 0
Metric Value
dl 0
loc 498
rs 4.8717
c 0
b 0
f 0
wmc 81
lcom 3
cbo 8

19 Methods

Rating   Name   Duplication   Size   Complexity  
A allClasses() 0 4 1
A exists() 0 4 3
A hasTable() 0 8 3
A reset_db_cache() 0 5 1
A getValidSubClasses() 0 15 4
B dataClassesFor() 0 23 5
A baseDataClass() 0 5 1
A subclassesFor() 0 17 4
A class_name() 0 8 2
C ancestry() 0 24 7
A implementorsOf() 0 4 1
A classImplements() 0 4 1
A classes_for_file() 0 14 3
A classes_for_folder() 0 14 3
B has_method_from() 0 22 4
A table_for_object_field() 0 5 1
A shortName() 0 5 1
A hasMethod() 0 10 4
D parse_class_spec() 0 135 32

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

1
<?php
2
3
namespace SilverStripe\Core;
4
5
use Exception;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Manifest\ClassLoader;
8
use SilverStripe\Dev\Deprecation;
9
use SilverStripe\ORM\ArrayLib;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\ORM\DataObject;
12
use ReflectionClass;
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
    /**
25
     * Wrapper for classes getter.
26
     *
27
     * @return array
28
     */
29
    public static function allClasses()
30
    {
31
        return ClassLoader::inst()->getManifest()->getClasses();
32
    }
33
34
    /**
35
     * Returns true if a class or interface name exists.
36
     *
37
     * @param  string $class
38
     * @return bool
39
     */
40
    public static function exists($class)
41
    {
42
        return class_exists($class, false) || interface_exists($class, false) || ClassLoader::inst()->getItemPath($class);
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\Core\Manif...()->getItemPath($class) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
43
    }
44
45
    /**
46
     * Cache for {@link hasTable()}
47
     *
48
     * @internal
49
     * @var array
50
     */
51
    private static $_cache_all_tables = array();
52
53
    /**
54
     * @internal
55
     * @var array Cache for {@link ancestry()}.
56
     */
57
    private static $_cache_ancestry = array();
58
59
    /**
60
     * Cache for parse_class_spec
61
     *
62
     * @internal
63
     * @var array
64
     */
65
    private static $_cache_parse = [];
66
67
    /**
68
     * @todo Move this to SS_Database or DB
69
     *
70
     * @param string $tableName
71
     * @return bool
72
     */
73
    public static function hasTable($tableName)
74
    {
75
        // Cache the list of all table names to reduce on DB traffic
76
        if (empty(self::$_cache_all_tables) && DB::is_active()) {
77
            self::$_cache_all_tables = DB::get_schema()->tableList();
78
        }
79
        return !empty(self::$_cache_all_tables[strtolower($tableName)]);
80
    }
81
82
    public static function reset_db_cache()
83
    {
84
        self::$_cache_all_tables = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $_cache_all_tables.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
85
        self::$_cache_ancestry = array();
86
    }
87
88
    /**
89
     * Returns the manifest of all classes which are present in the database.
90
     *
91
     * @param string $class Class name to check enum values for ClassName field
92
     * @param boolean $includeUnbacked Flag indicating whether or not to include
93
     * types that don't exist as implemented classes. By default these are excluded.
94
     * @return array List of subclasses
95
     */
96
    public static function getValidSubClasses($class = 'SilverStripe\\CMS\\Model\\SiteTree', $includeUnbacked = false)
97
    {
98
        if (is_string($class) && !class_exists($class)) {
99
            return array();
100
        }
101
102
        $class = self::class_name($class);
103
        if ($includeUnbacked) {
104
            $table = DataObject::getSchema()->tableName($class);
105
            $classes = DB::get_schema()->enumValuesForField($table, 'ClassName');
106
        } else {
107
            $classes = static::subclassesFor($class);
108
        }
109
        return $classes;
110
    }
111
112
    /**
113
     * Returns an array of the current class and all its ancestors and children
114
     * which require a DB table.
115
     *
116
     * @todo Move this into {@see DataObjectSchema}
117
     *
118
     * @param string|object $nameOrObject Class or object instance
119
     * @return array
120
     */
121
    public static function dataClassesFor($nameOrObject)
122
    {
123
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
124
            return array();
125
        }
126
127
        $result = array();
128
129
        $class = self::class_name($nameOrObject);
130
131
        $classes = array_merge(
132
            self::ancestry($class),
133
            self::subclassesFor($class)
134
        );
135
136
        foreach ($classes as $class) {
137
            if (DataObject::getSchema()->classHasTable($class)) {
138
                $result[$class] = $class;
139
            }
140
        }
141
142
        return $result;
143
    }
144
145
    /**
146
     * @deprecated 4.0..5.0
147
     */
148
    public static function baseDataClass($class)
149
    {
150
        Deprecation::notice('5.0', 'Use DataObject::getSchema()->baseDataClass()');
151
        return DataObject::getSchema()->baseDataClass($class);
152
    }
153
154
    /**
155
     * Returns a list of classes that inherit from the given class.
156
     * The resulting array includes the base class passed
157
     * through the $class parameter as the first array value.
158
     *
159
     * Example usage:
160
     * <code>
161
     * ClassInfo::subclassesFor('BaseClass');
162
     *  array(
163
     *  'BaseClass' => 'BaseClass',
164
     *  'ChildClass' => 'ChildClass',
165
     *  'GrandChildClass' => 'GrandChildClass'
166
     * )
167
     * </code>
168
     *
169
     * @param string|object $nameOrObject The classname or object
170
     * @return array Names of all subclasses as an associative array.
171
     */
172
    public static function subclassesFor($nameOrObject)
173
    {
174
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
175
            return [];
176
        }
177
178
        //normalise class case
179
        $className = self::class_name($nameOrObject);
180
        $descendants = ClassLoader::inst()->getManifest()->getDescendantsOf($className);
181
        $result      = array($className => $className);
182
183
        if ($descendants) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $descendants 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...
184
            return $result + ArrayLib::valuekey($descendants);
185
        } else {
186
            return $result;
187
        }
188
    }
189
190
    /**
191
     * Convert a class name in any case and return it as it was defined in PHP
192
     *
193
     * eg: self::class_name('dataobJEct'); //returns 'DataObject'
194
     *
195
     * @param string|object $nameOrObject The classname or object you want to normalise
196
     * @return string The normalised class name
197
     */
198
    public static function class_name($nameOrObject)
199
    {
200
        if (is_object($nameOrObject)) {
201
            return get_class($nameOrObject);
202
        }
203
        $reflection = new ReflectionClass($nameOrObject);
204
        return $reflection->getName();
205
    }
206
207
    /**
208
     * Returns the passed class name along with all its parent class names in an
209
     * array, sorted with the root class first.
210
     *
211
     * @param string|object $nameOrObject Class or object instance
212
     * @param bool $tablesOnly Only return classes that have a table in the db.
213
     * @return array
214
     */
215
    public static function ancestry($nameOrObject, $tablesOnly = false)
216
    {
217
        if (is_string($nameOrObject) && !class_exists($nameOrObject)) {
218
            return array();
219
        }
220
221
        $class = self::class_name($nameOrObject);
222
223
        $lClass = strtolower($class);
224
225
        $cacheKey = $lClass . '_' . (string)$tablesOnly;
226
        $parent = $class;
227
        if (!isset(self::$_cache_ancestry[$cacheKey])) {
228
            $ancestry = array();
229
            do {
230
                if (!$tablesOnly || DataObject::getSchema()->classHasTable($parent)) {
231
                    $ancestry[$parent] = $parent;
232
                }
233
            } while ($parent = get_parent_class($parent));
234
            self::$_cache_ancestry[$cacheKey] = array_reverse($ancestry);
235
        }
236
237
        return self::$_cache_ancestry[$cacheKey];
238
    }
239
240
    /**
241
     * @param string $interfaceName
242
     * @return array A self-keyed array of class names. Note that this is only available with Silverstripe
243
     * classes and not built-in PHP classes.
244
     */
245
    public static function implementorsOf($interfaceName)
246
    {
247
        return ClassLoader::inst()->getManifest()->getImplementorsOf($interfaceName);
248
    }
249
250
    /**
251
     * Returns true if the given class implements the given interface
252
     *
253
     * @param string $className
254
     * @param string $interfaceName
255
     * @return bool
256
     */
257
    public static function classImplements($className, $interfaceName)
258
    {
259
        return in_array($className, self::implementorsOf($interfaceName));
260
    }
261
262
    /**
263
     * Get all classes contained in a file.
264
     * @uses ManifestBuilder
265
     *
266
     * @todo Doesn't return additional classes that only begin
267
     *  with the filename, and have additional naming separated through underscores.
268
     *
269
     * @param string $filePath Path to a PHP file (absolute or relative to webroot)
270
     * @return array
271
     */
272
    public static function classes_for_file($filePath)
273
    {
274
        $absFilePath    = Director::getAbsFile($filePath);
275
        $matchedClasses = array();
276
        $manifest       = ClassLoader::inst()->getManifest()->getClasses();
277
278
        foreach ($manifest as $class => $compareFilePath) {
279
            if ($absFilePath == $compareFilePath) {
280
                $matchedClasses[] = $class;
281
            }
282
        }
283
284
        return $matchedClasses;
285
    }
286
287
    /**
288
     * Returns all classes contained in a certain folder.
289
     *
290
     * @todo Doesn't return additional classes that only begin
291
     *  with the filename, and have additional naming separated through underscores.
292
     *
293
     * @param string $folderPath Relative or absolute folder path
294
     * @return array Array of class names
295
     */
296
    public static function classes_for_folder($folderPath)
297
    {
298
        $absFolderPath  = Director::getAbsFile($folderPath);
299
        $matchedClasses = array();
300
        $manifest       = ClassLoader::inst()->getManifest()->getClasses();
301
302
        foreach ($manifest as $class => $compareFilePath) {
303
            if (stripos($compareFilePath, $absFolderPath) === 0) {
304
                $matchedClasses[] = $class;
305
            }
306
        }
307
308
        return $matchedClasses;
309
    }
310
311
    private static $method_from_cache = array();
312
313
    public static function has_method_from($class, $method, $compclass)
314
    {
315
        $lClass = strtolower($class);
316
        $lMethod = strtolower($method);
317
        $lCompclass = strtolower($compclass);
318
        if (!isset(self::$method_from_cache[$lClass])) {
319
            self::$method_from_cache[$lClass] = array();
320
        }
321
322
        if (!array_key_exists($lMethod, self::$method_from_cache[$lClass])) {
323
            self::$method_from_cache[$lClass][$lMethod] = false;
324
325
            $classRef = new ReflectionClass($class);
326
327
            if ($classRef->hasMethod($method)) {
328
                $methodRef = $classRef->getMethod($method);
329
                self::$method_from_cache[$lClass][$lMethod] = $methodRef->getDeclaringClass()->getName();
330
            }
331
        }
332
333
        return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
334
    }
335
336
    /**
337
     * @deprecated 4.0..5.0
338
     */
339
    public static function table_for_object_field($candidateClass, $fieldName)
340
    {
341
        Deprecation::notice('5.0', 'Use DataObject::getSchema()->tableForField()');
342
        return DataObject::getSchema()->tableForField($candidateClass, $fieldName);
343
    }
344
345
    /**
346
     * Strip namespace from class
347
     *
348
     * @param string|object $nameOrObject Name of class, or instance
349
     * @return string Name of class without namespace
350
     */
351
    public static function shortName($nameOrObject)
352
    {
353
        $reflection = new ReflectionClass($nameOrObject);
354
        return $reflection->getShortName();
355
    }
356
357
    /**
358
     * Helper to determine if the given object has a method
359
     *
360
     * @param object $object
361
     * @param string $method
362
     * @return bool
363
     */
364
    public static function hasMethod($object, $method)
365
    {
366
        if (empty($object)) {
367
            return false;
368
        }
369
        if (method_exists($object, $method)) {
370
            return true;
371
        }
372
        return method_exists($object, 'hasMethod') && $object->hasMethod($method);
373
    }
374
375
    /**
376
     * Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
377
     * Returns a 2-element array, with classname and arguments
378
     *
379
     * @param string $classSpec
380
     * @return array
381
     * @throws Exception
382
     */
383
    public static function parse_class_spec($classSpec)
384
    {
385
        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; consider using self, or increasing the visibility of $_cache_parse to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
386
            return 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; consider using self, or increasing the visibility of $_cache_parse to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
387
        }
388
389
        $tokens = token_get_all("<?php $classSpec");
390
        $class = null;
391
        $args = array();
392
393
        // Keep track of the current bucket that we're putting data into
394
        $bucket = &$args;
395
        $bucketStack = array();
396
        $hadNamespace = false;
397
        $currentKey = null;
398
399
        foreach ($tokens as $token) {
400
            // $forceResult used to allow null result to be detected
401
            $result = $forceResult = null;
402
            $tokenName = is_array($token) ? $token[0] : $token;
403
404
            // Get the class name
405
            if ($class === null && is_array($token) && $token[0] === T_STRING) {
406
                $class = $token[1];
407
            } elseif (is_array($token) && $token[0] === T_NS_SEPARATOR) {
408
                $class .= $token[1];
409
                $hadNamespace = true;
410
            } elseif ($hadNamespace && is_array($token) && $token[0] === T_STRING) {
411
                $class .= $token[1];
412
                $hadNamespace = false;
413
            // Get arguments
414
            } elseif (is_array($token)) {
415
                switch ($token[0]) {
416
                    case T_CONSTANT_ENCAPSED_STRING:
417
                        $argString = $token[1];
418
                        switch ($argString[0]) {
419
                            case '"':
420
                                $result = stripcslashes(substr($argString, 1, -1));
421
                                break;
422
                            case "'":
423
                                $result = str_replace(array("\\\\", "\\'"), array("\\", "'"), substr($argString, 1, -1));
424
                                break;
425
                            default:
426
                                throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
427
                        }
428
429
                        break;
430
431
                    case T_DNUMBER:
432
                        $result = (double)$token[1];
433
                        break;
434
435
                    case T_LNUMBER:
436
                        $result = (int)$token[1];
437
                        break;
438
439
                    case T_DOUBLE_ARROW:
440
                        // We've encountered an associative array (the array itself has already been
441
                        // added to the bucket), so the previous item added to the bucket is the key
442
                        end($bucket);
443
                        $currentKey = current($bucket);
444
                        array_pop($bucket);
445
                        break;
446
447
                    case T_STRING:
448
                        switch ($token[1]) {
449
                            case 'true':
450
                                $result = true;
451
452
                                break;
453
                            case 'false':
454
                                $result = false;
455
456
                                break;
457
                            case 'null':
458
                                $result = null;
459
                                $forceResult = true;
460
461
                                break;
462
                            default:
463
                                throw new Exception("Bad T_STRING arg '{$token[1]}'");
464
                        }
465
466
                        break;
467
468
                    case T_ARRAY:
469
                        $result = array();
470
                        break;
471
                }
472
            } else {
473
                if ($tokenName === '[') {
474
                    $result = array();
475
                } elseif (($tokenName === ')' || $tokenName === ']') && ! empty($bucketStack)) {
476
                    // Store the bucket we're currently working on
477
                    $oldBucket = $bucket;
478
                    // Fetch the key for the bucket at the top of the stack
479
                    end($bucketStack);
480
                    $key = key($bucketStack);
481
                    reset($bucketStack);
482
                    // Re-instate the bucket from the top of the stack
483
                    $bucket = &$bucketStack[$key];
484
                    // Add our saved, "nested" bucket to the bucket we just popped off the stack
485
                    $bucket[$key] = $oldBucket;
486
                    // Remove the bucket we just popped off the stack
487
                    array_pop($bucketStack);
488
                }
489
            }
490
491
            // If we've got something to add to the bucket, add it
492
            if ($result !== null || $forceResult) {
493
                if ($currentKey) {
494
                    $bucket[$currentKey] = $result;
495
                    $currentKey = null;
496
                } else {
497
                    $bucket[] = $result;
498
                }
499
500
                // If we've just pushed an array, that becomes our new bucket
501
                if ($result === array()) {
502
                    // Fetch the key that the array was pushed to
503
                    end($bucket);
504
                    $key = key($bucket);
505
                    reset($bucket);
506
                    // Store reference to "old" bucket in the stack
507
                    $bucketStack[$key] = &$bucket;
508
                    // Set the active bucket to be our newly-pushed, empty array
509
                    $bucket = &$bucket[$key];
510
                }
511
            }
512
        }
513
514
        $result = [$class, $args];
515
        static::$_cache_parse[$classSpec] = $result;
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; consider using self, or increasing the visibility of $_cache_parse to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
516
        return $result;
517
    }
518
}
519