Completed
Push — authenticator-refactor ( 0a18bb...b9e528 )
by Simon
08:12
created

ClassInfo::parse_class_spec()   D

Complexity

Conditions 32
Paths 166

Size

Total Lines 135
Code Lines 90

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 32
eloc 90
nc 166
nop 1
dl 0
loc 135
rs 4.1588
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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();
0 ignored issues
show
Bug introduced by
Consider using $reflection->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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 View Code Duplication
    public static function classes_for_file($filePath)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
    public static function classes_for_folder($folderPath)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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();
0 ignored issues
show
introduced by
Consider using $methodRef->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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