Passed
Push — fix-8832 ( 2eb5fa )
by Sam
07:48
created

CustomMethods::getExtraMethodConfig()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
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']):
64
                return $config['callback']($this, $arguments);
65
            case isset($config['property']):
66
                $property = $config['property'];
67
                $index = $config['index'];
68
                $obj = $index !== null ?
69
                    $this->{$property}[$index] :
70
                    $this->{$property};
71
72
                if (!$obj) {
73
                    throw new BadMethodCallException(
74
                        "Object->__call(): {$class} cannot pass control to {$property}({$index})."
75
                        . ' Perhaps this object was mistakenly destroyed?'
76
                    );
77
                }
78
79
                // Call on object
80
                try {
81
                    if ($obj instanceof Extension) {
82
                        $obj->setOwner($this);
83
                    }
84
                    return $obj->$method(...$arguments);
85
                } finally {
86
                    if ($obj instanceof Extension) {
87
                        $obj->clearOwner();
88
                    }
89
                }
90
                break;
91
            case isset($config['wrap']):
92
                array_unshift($arguments, $config['method']);
93
                $wrapped = $config['wrap'];
94
                return $this->$wrapped(...$arguments);
95
            case isset($config['function']):
96
                return $config['function']($this, $arguments);
97
            default:
98
                throw new BadMethodCallException(
99
                    "Object->__call(): extra method $method is invalid on $class:"
100
                    . var_export($config, true)
101
                );
102
        }
103
    }
104
105
    /**
106
     * Adds any methods from {@link Extension} instances attached to this object.
107
     * All these methods can then be called directly on the instance (transparently
108
     * mapped through {@link __call()}), or called explicitly through {@link extend()}.
109
     *
110
     * @uses addMethodsFrom()
111
     */
112
    protected function defineMethods()
113
    {
114
        // Define from all registered callbacks
115
        foreach ($this->extra_method_registers as $callback) {
116
            call_user_func($callback);
117
        }
118
    }
119
120
    /**
121
     * Register an callback to invoke that defines extra methods
122
     *
123
     * @param string $name
124
     * @param callable $callback
125
     */
126
    protected function registerExtraMethodCallback($name, $callback)
127
    {
128
        if (!isset($this->extra_method_registers[$name])) {
129
            $this->extra_method_registers[$name] = $callback;
130
        }
131
    }
132
133
    // --------------------------------------------------------------------------------------------------------------
134
135
    /**
136
     * Return TRUE if a method exists on this object
137
     *
138
     * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
139
     * extensions
140
     *
141
     * @param string $method
142
     * @return bool
143
     */
144
    public function hasMethod($method)
145
    {
146
        return method_exists($this, $method) || $this->getExtraMethodConfig($method);
147
    }
148
149
    /**
150
     * Get meta-data details on a named method
151
     *
152
     * @param string $method
153
     * @return array List of custom method details, if defined for this method
154
     */
155
    protected function getExtraMethodConfig($method)
156
    {
157
        // Lazy define methods
158
        $lowerClass = strtolower(static::class);
159
        if (!isset(self::$extra_methods[$lowerClass])) {
160
            $this->defineMethods();
161
        }
162
163
        return self::$extra_methods[$lowerClass][strtolower($method)] ?? null;
164
    }
165
166
    /**
167
     * Return the names of all the methods available on this object
168
     *
169
     * @param bool $custom include methods added dynamically at runtime
170
     * @return array Map of method names with lowercase keys
171
     */
172
    public function allMethodNames($custom = false)
173
    {
174
        $methods = static::findBuiltInMethods();
175
176
        // Query extra methods
177
        $lowerClass = strtolower(static::class);
178
        if ($custom && isset(self::$extra_methods[$lowerClass])) {
179
            $methods = array_merge(self::$extra_methods[$lowerClass], $methods);
180
        }
181
182
        return $methods;
183
    }
184
185
    /**
186
     * Get all public built in methods for this class
187
     *
188
     * @param string|object $class Class or instance to query methods from (defaults to static::class)
189
     * @return array Map of methods with lowercase key name
190
     */
191
    protected static function findBuiltInMethods($class = null)
192
    {
193
        $class = is_object($class) ? get_class($class) : ($class ?: static::class);
194
        $lowerClass = strtolower($class);
195
        if (isset(self::$built_in_methods[$lowerClass])) {
196
            return self::$built_in_methods[$lowerClass];
197
        }
198
199
        // Build new list
200
        $reflection = new ReflectionClass($class);
201
        $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
202
        self::$built_in_methods[$lowerClass] = [];
203
        foreach ($methods as $method) {
204
            $name = $method->getName();
205
            self::$built_in_methods[$lowerClass][strtolower($name)] = $name;
206
        }
207
        return self::$built_in_methods[$lowerClass];
208
    }
209
210
    /**
211
     * Find all methods on the given object.
212
     *
213
     * @param object $object
214
     * @return array
215
     */
216
    protected function findMethodsFrom($object)
217
    {
218
        // Respect "allMethodNames"
219
        if (method_exists($object, 'allMethodNames')) {
220
            if ($object instanceof Extension) {
221
                try {
222
                    $object->setOwner($this);
223
                    $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

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