Completed
Push — 4.0 ( b59aea...80f83b )
by Loz
52s queued 21s
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']): {
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...
61
                return $config['callback']($this, $arguments);
62
            }
63
            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...
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']): {
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...
91
                array_unshift($arguments, $config['method']);
92
                $wrapped = $config['wrap'];
93
                return $this->$wrapped(...$arguments);
94
            }
95
            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...
96
                return $config['function']($this, $arguments);
97
            }
98
            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...
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
                $methodInfo = self::$extra_methods[$class][$method];
283
284
                if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
285
                    unset(self::$extra_methods[$class][$method]);
286
                }
287
            }
288
289
            if (empty(self::$extra_methods[$class])) {
290
                unset(self::$extra_methods[$class]);
291
            }
292
        }
293
    }
294
295
    /**
296
     * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
297
     * can be wrapped to generateThumbnail(x)
298
     *
299
     * @param string $method the method name to wrap
300
     * @param string $wrap the method name to wrap to
301
     */
302
    protected function addWrapperMethod($method, $wrap)
303
    {
304
        self::$extra_methods[static::class][strtolower($method)] = array(
305
            'wrap' => $wrap,
306
            'method' => $method
307
        );
308
    }
309
310
    /**
311
     * Add callback as a method.
312
     *
313
     * @param string $method Name of method
314
     * @param callable $callback Callback to invoke.
315
     * Note: $this is passed as first parameter to this callback and then $args as array
316
     */
317
    protected function addCallbackMethod($method, $callback)
318
    {
319
        self::$extra_methods[static::class][strtolower($method)] = [
320
            'callback' => $callback,
321
        ];
322
    }
323
}
324