Passed
Push — zero-is-false ( 5bf3f2...d7e1e1 )
by Sam
08:16
created

CustomMethods::findMethodsFromExtension()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 6
nop 1
dl 0
loc 22
rs 9.7666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core;
4
5
use BadMethodCallException;
6
use InvalidArgumentException;
7
use SilverStripe\Dev\Deprecation;
8
9
/**
10
 * Allows an object to declare a set of custom methods
11
 */
12
trait CustomMethods
13
{
14
15
    /**
16
     * Custom method sources
17
     *
18
     * @var array
19
     */
20
    protected static $extra_methods = [];
21
22
    /**
23
     * Name of methods to invoke by defineMethods for this instance
24
     *
25
     * @var array
26
     */
27
    protected $extra_method_registers = array();
28
29
    /**
30
     * Non-custom methods
31
     *
32
     * @var array
33
     */
34
    protected static $built_in_methods = array();
35
36
    /**
37
     * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
38
     *
39
     * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
40
     * {@link Object::addWrapperMethod()}
41
     *
42
     * @param string $method
43
     * @param array $arguments
44
     * @return mixed
45
     * @throws BadMethodCallException
46
     */
47
    public function __call($method, $arguments)
48
    {
49
        // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
50
        // call, then we should rebuild it.
51
        $class = static::class;
52
        $config = $this->getExtraMethodConfig($method);
53
        if (empty($config)) {
54
            throw new BadMethodCallException(
55
                "Object->__call(): the method '$method' does not exist on '$class'"
56
            );
57
        }
58
59
        switch (true) {
60
            case isset($config['callback']): {
61
                return $config['callback']($this, $arguments);
62
            }
63
            case isset($config['property']) : {
64
                $property = $config['property'];
65
                $index = $config['index'];
66
                $obj = $index !== null ?
67
                    $this->{$property}[$index] :
68
                    $this->{$property};
69
70
                if (!$obj) {
71
                    throw new BadMethodCallException(
72
                        "Object->__call(): {$class} cannot pass control to {$property}({$index})."
73
                        . ' Perhaps this object was mistakenly destroyed?'
74
                    );
75
                }
76
77
                // Call without setOwner
78
                if (empty($config['callSetOwnerFirst'])) {
79
                    return $obj->$method(...$arguments);
80
                }
81
82
                /** @var Extension $obj */
83
                try {
84
                    $obj->setOwner($this);
85
                    return $obj->$method(...$arguments);
86
                } finally {
87
                    $obj->clearOwner();
88
                }
89
            }
90
            case isset($config['wrap']): {
91
                array_unshift($arguments, $config['method']);
92
                $wrapped = $config['wrap'];
93
                return $this->$wrapped(...$arguments);
94
            }
95
            case isset($config['function']): {
96
                return $config['function']($this, $arguments);
97
            }
98
            default: {
99
                throw new BadMethodCallException(
100
                    "Object->__call(): extra method $method is invalid on $class:"
101
                    . var_export($config, true)
102
                );
103
            }
104
        }
105
    }
106
107
    /**
108
     * Adds any methods from {@link Extension} instances attached to this object.
109
     * All these methods can then be called directly on the instance (transparently
110
     * mapped through {@link __call()}), or called explicitly through {@link extend()}.
111
     *
112
     * @uses addMethodsFrom()
113
     */
114
    protected function defineMethods()
115
    {
116
        // Define from all registered callbacks
117
        foreach ($this->extra_method_registers as $callback) {
118
            call_user_func($callback);
119
        }
120
    }
121
122
    /**
123
     * Register an callback to invoke that defines extra methods
124
     *
125
     * @param string $name
126
     * @param callable $callback
127
     */
128
    protected function registerExtraMethodCallback($name, $callback)
129
    {
130
        if (!isset($this->extra_method_registers[$name])) {
131
            $this->extra_method_registers[$name] = $callback;
132
        }
133
    }
134
135
    // --------------------------------------------------------------------------------------------------------------
136
137
    /**
138
     * Return TRUE if a method exists on this object
139
     *
140
     * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
141
     * extensions
142
     *
143
     * @param string $method
144
     * @return bool
145
     */
146
    public function hasMethod($method)
147
    {
148
        return method_exists($this, $method) || $this->getExtraMethodConfig($method);
149
    }
150
151
    /**
152
     * Get meta-data details on a named method
153
     *
154
     * @param string $method
155
     * @return array List of custom method details, if defined for this method
156
     */
157
    protected function getExtraMethodConfig($method)
158
    {
159
        // Lazy define methods
160
        if (!isset(self::$extra_methods[static::class])) {
161
            $this->defineMethods();
162
        }
163
164
        if (isset(self::$extra_methods[static::class][strtolower($method)])) {
165
            return self::$extra_methods[static::class][strtolower($method)];
166
        }
167
        return 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
175
     */
176
    public function allMethodNames($custom = false)
177
    {
178
        $class = static::class;
179
        if (!isset(self::$built_in_methods[$class])) {
180
            self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
181
        }
182
183
        if ($custom && isset(self::$extra_methods[$class])) {
184
            return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
185
        } else {
186
            return self::$built_in_methods[$class];
187
        }
188
    }
189
190
    /**
191
     * @param object $extension
192
     * @return array
193
     */
194
    protected function findMethodsFromExtension($extension)
195
    {
196
        if (method_exists($extension, 'allMethodNames')) {
197
            if ($extension instanceof Extension) {
198
                try {
199
                    $extension->setOwner($this);
200
                    $methods = $extension->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

200
                    /** @scrutinizer ignore-call */ 
201
                    $methods = $extension->allMethodNames(true);
Loading history...
201
                } finally {
202
                    $extension->clearOwner();
203
                }
204
            } else {
205
                $methods = $extension->allMethodNames(true);
206
            }
207
        } else {
208
            $class = get_class($extension);
209
            if (!isset(self::$built_in_methods[$class])) {
210
                self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($extension));
211
            }
212
            $methods = self::$built_in_methods[$class];
213
        }
214
215
        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...
216
    }
217
218
    /**
219
     * Add all the methods from an object property (which is an {@link Extension}) to this object.
220
     *
221
     * @param string $property the property name
222
     * @param string|int $index an index to use if the property is an array
223
     * @throws InvalidArgumentException
224
     */
225
    protected function addMethodsFrom($property, $index = null)
226
    {
227
        $class = static::class;
228
        $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
229
230
        if (!$extension) {
231
            throw new InvalidArgumentException(
232
                "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
233
            );
234
        }
235
236
        $methods = $this->findMethodsFromExtension($extension);
237
        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...
238
            if ($extension instanceof Extension) {
239
                Deprecation::notice(
240
                    '5.0',
241
                    'Register custom methods from extensions with addCallbackMethod.'
242
                    . ' callSetOwnerFirst will be removed in 5.0'
243
                );
244
            }
245
            $methodInfo = array(
246
                'property' => $property,
247
                'index' => $index,
248
                'callSetOwnerFirst' => $extension instanceof Extension,
249
            );
250
251
            $newMethods = array_fill_keys($methods, $methodInfo);
252
253
            if (isset(self::$extra_methods[$class])) {
254
                self::$extra_methods[$class] =
255
                    array_merge(self::$extra_methods[$class], $newMethods);
256
            } else {
257
                self::$extra_methods[$class] = $newMethods;
258
            }
259
        }
260
    }
261
262
    /**
263
     * Add all the methods from an object property (which is an {@link Extension}) to this object.
264
     *
265
     * @param string $property the property name
266
     * @param string|int $index an index to use if the property is an array
267
     */
268
    protected function removeMethodsFrom($property, $index = null)
269
    {
270
        $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
271
        $class = static::class;
272
273
        if (!$extension) {
274
            throw new InvalidArgumentException(
275
                "Object->removeMethodsFrom(): could not remove methods from {$class}->{$property}[$index]"
276
            );
277
        }
278
279
        $methods = $this->findMethodsFromExtension($extension);
280
        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...
281
            foreach ($methods as $method) {
282
                if (!isset(self::$extra_methods[$class][$method])) {
283
                    continue;
284
                }
285
                    
286
                $methodInfo = self::$extra_methods[$class][$method];
287
288
                if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
289
                    unset(self::$extra_methods[$class][$method]);
290
                }
291
            }
292
293
            if (empty(self::$extra_methods[$class])) {
294
                unset(self::$extra_methods[$class]);
295
            }
296
        }
297
    }
298
299
    /**
300
     * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
301
     * can be wrapped to generateThumbnail(x)
302
     *
303
     * @param string $method the method name to wrap
304
     * @param string $wrap the method name to wrap to
305
     */
306
    protected function addWrapperMethod($method, $wrap)
307
    {
308
        self::$extra_methods[static::class][strtolower($method)] = array(
309
            'wrap' => $wrap,
310
            'method' => $method
311
        );
312
    }
313
314
    /**
315
     * Add callback as a method.
316
     *
317
     * @param string $method Name of method
318
     * @param callable $callback Callback to invoke.
319
     * Note: $this is passed as first parameter to this callback and then $args as array
320
     */
321
    protected function addCallbackMethod($method, $callback)
322
    {
323
        self::$extra_methods[static::class][strtolower($method)] = [
324
            'callback' => $callback,
325
        ];
326
    }
327
}
328