Passed
Push — master ( ea68ff...25edad )
by Daniel
12:24 queued 10s
created

CustomMethods::__call()   C

Complexity

Conditions 10
Paths 27

Size

Total Lines 54
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 35
nc 27
nop 2
dl 0
loc 54
rs 6.8372
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 BadMethodCallException;
6
use InvalidArgumentException;
7
use ReflectionClass;
8
use ReflectionMethod;
9
10
/**
11
 * Allows an object to declare a set of custom methods
12
 */
13
trait CustomMethods
14
{
15
    /**
16
     * Custom method sources
17
     *
18
     * @var array Array of class names (lowercase) to list of methods.
19
     * The list of methods will have lowercase keys. Each value in this array
20
     * can be a callable, array, or string callback
21
     */
22
    protected static $extra_methods = [];
23
24
    /**
25
     * Name of methods to invoke by defineMethods for this instance
26
     *
27
     * @var array
28
     */
29
    protected $extra_method_registers = [];
30
31
    /**
32
     * Non-custom public methods.
33
     *
34
     * @var array Array of class names (lowercase) to list of methods.
35
     * The list of methods will have lowercase keys and correct-case values.
36
     */
37
    protected static $built_in_methods = array();
38
39
    /**
40
     * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
41
     *
42
     * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
43
     * {@link Object::addWrapperMethod()}
44
     *
45
     * @param string $method
46
     * @param array $arguments
47
     * @return mixed
48
     * @throws BadMethodCallException
49
     */
50
    public function __call($method, $arguments)
51
    {
52
        // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
53
        // call, then we should rebuild it.
54
        $class = static::class;
55
        $config = $this->getExtraMethodConfig($method);
56
        if (empty($config)) {
57
            throw new BadMethodCallException(
58
                "Object->__call(): the method '$method' does not exist on '$class'"
59
            );
60
        }
61
62
        switch (true) {
63
            case isset($config['callback']): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
64
                return $config['callback']($this, $arguments);
65
            }
66
            case isset($config['property']) : {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
67
                $property = $config['property'];
68
                $index = $config['index'];
69
                $obj = $index !== null ?
70
                    $this->{$property}[$index] :
71
                    $this->{$property};
72
73
                if (!$obj) {
74
                    throw new BadMethodCallException(
75
                        "Object->__call(): {$class} cannot pass control to {$property}({$index})."
76
                        . ' Perhaps this object was mistakenly destroyed?'
77
                    );
78
                }
79
80
                // Call on object
81
                try {
82
                    if ($obj instanceof Extension) {
83
                        $obj->setOwner($this);
84
                    }
85
                    return $obj->$method(...$arguments);
86
                } finally {
87
                    if ($obj instanceof Extension) {
88
                        $obj->clearOwner();
89
                    }
90
                }
91
            }
92
            case isset($config['wrap']): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
93
                array_unshift($arguments, $config['method']);
94
                $wrapped = $config['wrap'];
95
                return $this->$wrapped(...$arguments);
96
            }
97
            case isset($config['function']): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
98
                return $config['function']($this, $arguments);
99
            }
100
            default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
101
                throw new BadMethodCallException(
102
                    "Object->__call(): extra method $method is invalid on $class:"
103
                    . var_export($config, true)
104
                );
105
            }
106
        }
107
    }
108
109
    /**
110
     * Adds any methods from {@link Extension} instances attached to this object.
111
     * All these methods can then be called directly on the instance (transparently
112
     * mapped through {@link __call()}), or called explicitly through {@link extend()}.
113
     *
114
     * @uses addMethodsFrom()
115
     */
116
    protected function defineMethods()
117
    {
118
        // Define from all registered callbacks
119
        foreach ($this->extra_method_registers as $callback) {
120
            call_user_func($callback);
121
        }
122
    }
123
124
    /**
125
     * Register an callback to invoke that defines extra methods
126
     *
127
     * @param string $name
128
     * @param callable $callback
129
     */
130
    protected function registerExtraMethodCallback($name, $callback)
131
    {
132
        if (!isset($this->extra_method_registers[$name])) {
133
            $this->extra_method_registers[$name] = $callback;
134
        }
135
    }
136
137
    // --------------------------------------------------------------------------------------------------------------
138
139
    /**
140
     * Return TRUE if a method exists on this object
141
     *
142
     * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
143
     * extensions
144
     *
145
     * @param string $method
146
     * @return bool
147
     */
148
    public function hasMethod($method)
149
    {
150
        return method_exists($this, $method) || $this->getExtraMethodConfig($method);
151
    }
152
153
    /**
154
     * Get meta-data details on a named method
155
     *
156
     * @param string $method
157
     * @return array List of custom method details, if defined for this method
158
     */
159
    protected function getExtraMethodConfig($method)
160
    {
161
        // Lazy define methods
162
        $lowerClass = strtolower(static::class);
163
        if (!isset(self::$extra_methods[$lowerClass])) {
164
            $this->defineMethods();
165
        }
166
167
        return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
168
    }
169
170
    /**
171
     * Return the names of all the methods available on this object
172
     *
173
     * @param bool $custom include methods added dynamically at runtime
174
     * @return array Map of method names with lowercase keys
175
     */
176
    public function allMethodNames($custom = false)
177
    {
178
        $methods = static::findBuiltInMethods();
179
180
        // Query extra methods
181
        $lowerClass = strtolower(static::class);
182
        if ($custom && isset(self::$extra_methods[$lowerClass])) {
183
            $methods = array_merge(self::$extra_methods[$lowerClass], $methods);
184
        }
185
186
        return $methods;
187
    }
188
189
    /**
190
     * Get all public built in methods for this class
191
     *
192
     * @param string|object $class Class or instance to query methods from (defaults to static::class)
193
     * @return array Map of methods with lowercase key name
194
     */
195
    protected static function findBuiltInMethods($class = null)
196
    {
197
        $class = is_object($class) ? get_class($class) : ($class ?: static::class);
198
        $lowerClass = strtolower($class);
199
        if (isset(self::$built_in_methods[$lowerClass])) {
200
            return self::$built_in_methods[$lowerClass];
201
        }
202
203
        // Build new list
204
        $reflection = new ReflectionClass($class);
205
        $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
206
        self::$built_in_methods[$lowerClass] = [];
207
        foreach ($methods as $method) {
208
            $name = $method->getName();
209
            self::$built_in_methods[$lowerClass][strtolower($name)] = $name;
210
        }
211
        return self::$built_in_methods[$lowerClass];
212
    }
213
214
    /**
215
     * Find all methods on the given object.
216
     *
217
     * @param object $object
218
     * @return array
219
     */
220
    protected function findMethodsFrom($object)
221
    {
222
        // Respect "allMethodNames"
223
        if (method_exists($object, 'allMethodNames')) {
224
            if ($object instanceof Extension) {
225
                try {
226
                    $object->setOwner($this);
227
                    $methods = $object->allMethodNames(true);
0 ignored issues
show
Bug introduced by
The method allMethodNames() does not exist on SilverStripe\Core\Extension. It seems like you code against a sub-type of SilverStripe\Core\Extension such as SilverStripe\ORM\Tests\D...sionTest\AllMethodNames. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

227
                    /** @scrutinizer ignore-call */ 
228
                    $methods = $object->allMethodNames(true);
Loading history...
228
                } finally {
229
                    $object->clearOwner();
230
                }
231
            } else {
232
                $methods = $object->allMethodNames(true);
233
            }
234
            return $methods;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $methods does not seem to be defined for all execution paths leading up to this point.
Loading history...
235
        }
236
237
        // Get methods
238
        return static::findBuiltInMethods($object);
239
    }
240
241
    /**
242
     * Add all the methods from an object property.
243
     *
244
     * @param string $property the property name
245
     * @param string|int $index an index to use if the property is an array
246
     * @throws InvalidArgumentException
247
     */
248
    protected function addMethodsFrom($property, $index = null)
249
    {
250
        $class = static::class;
251
        $object = ($index !== null) ? $this->{$property}[$index] : $this->$property;
252
253
        if (!$object) {
254
            throw new InvalidArgumentException(
255
                "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
256
            );
257
        }
258
259
        $methods = $this->findMethodsFrom($object);
260
        if (!$methods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods 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...
261
            return;
262
        }
263
        $methodInfo = [
264
            'property' => $property,
265
            'index' => $index,
266
        ];
267
268
        $newMethods = array_fill_keys(array_keys($methods), $methodInfo);
269
270
        // Merge with extra_methods
271
        $lowerClass = strtolower($class);
272
        if (isset(self::$extra_methods[$lowerClass])) {
273
            self::$extra_methods[$lowerClass] = array_merge(self::$extra_methods[$lowerClass], $newMethods);
274
        } else {
275
            self::$extra_methods[$lowerClass] = $newMethods;
276
        }
277
    }
278
279
    /**
280
     * Add all the methods from an object property (which is an {@link Extension}) to this object.
281
     *
282
     * @param string $property the property name
283
     * @param string|int $index an index to use if the property is an array
284
     */
285
    protected function removeMethodsFrom($property, $index = null)
286
    {
287
        $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
288
        $class = static::class;
289
290
        if (!$extension) {
291
            throw new InvalidArgumentException(
292
                "Object->removeMethodsFrom(): could not remove methods from {$class}->{$property}[$index]"
293
            );
294
        }
295
296
        $lowerClass = strtolower($class);
297
        if (!isset(self::$extra_methods[$lowerClass])) {
298
            return;
299
        }
300
        $methods = $this->findMethodsFrom($extension);
301
302
        // Unset by key
303
        self::$extra_methods[$lowerClass] = array_diff_key(self::$extra_methods[$lowerClass], $methods);
304
305
        // Clear empty list
306
        if (empty(self::$extra_methods[$lowerClass])) {
307
            unset(self::$extra_methods[$lowerClass]);
308
        }
309
    }
310
311
    /**
312
     * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
313
     * can be wrapped to generateThumbnail(x)
314
     *
315
     * @param string $method the method name to wrap
316
     * @param string $wrap the method name to wrap to
317
     */
318
    protected function addWrapperMethod($method, $wrap)
319
    {
320
        self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
321
            'wrap' => $wrap,
322
            'method' => $method
323
        ];
324
    }
325
326
    /**
327
     * Add callback as a method.
328
     *
329
     * @param string $method Name of method
330
     * @param callable $callback Callback to invoke.
331
     * Note: $this is passed as first parameter to this callback and then $args as array
332
     */
333
    protected function addCallbackMethod($method, $callback)
334
    {
335
        self::$extra_methods[strtolower(static::class)][strtolower($method)] = [
336
            'callback' => $callback,
337
        ];
338
    }
339
}
340