Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

CustomMethods::createMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 7
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 7
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core;
4
5
use BadMethodCallException;
6
use InvalidArgumentException;
7
8
/**
9
 * Allows an object to declare a set of custom methods
10
 */
11
trait CustomMethods
12
{
13
14
    /**
15
     * Custom method sources
16
     *
17
     * @var array
18
     */
19
    protected static $extra_methods = array();
20
21
    /**
22
     * Name of methods to invoke by defineMethods for this instance
23
     *
24
     * @var array
25
     */
26
    protected $extra_method_registers = array();
27
28
    /**
29
     * Non-custom methods
30
     *
31
     * @var array
32
     */
33
    protected static $built_in_methods = array();
34
35
    /**
36
     * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
37
     *
38
     * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
39
     * {@link Object::addWrapperMethod()}
40
     *
41
     * @param string $method
42
     * @param array $arguments
43
     * @return mixed
44
     * @throws BadMethodCallException
45
     */
46
    public function __call($method, $arguments)
47
    {
48
        // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
49
        // call, then we should rebuild it.
50
        $class = get_class($this);
51
        if (!array_key_exists($class, self::$extra_methods)) {
52
            $this->defineMethods();
53
        }
54
55
        $config = $this->getExtraMethodConfig($method);
0 ignored issues
show
Documentation introduced by
$method is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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['property']) : {
64
                $obj = $config['index'] !== null ?
65
                    $this->{$config['property']}[$config['index']] :
66
                    $this->{$config['property']};
67
68
                if ($obj) {
69
                    if (!empty($config['callSetOwnerFirst'])) {
70
                        /** @var Extension $obj */
71
                        $obj->setOwner($this);
72
                    }
73
                    $retVal = call_user_func_array(array($obj, $method), $arguments);
74
                    if (!empty($config['callSetOwnerFirst'])) {
75
                        /** @var Extension $obj */
76
                        $obj->clearOwner();
77
                    }
78
                    return $retVal;
79
                }
80
81
                if (!empty($this->destroyed)) {
0 ignored issues
show
Bug introduced by
The property destroyed does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
82
                    throw new BadMethodCallException(
83
                        "Object->__call(): attempt to call $method on a destroyed $class object"
84
                    );
85
                } else {
86
                    throw new BadMethodCallException(
87
                        "Object->__call(): $class cannot pass control to $config[property]($config[index])."
88
                        . ' Perhaps this object was mistakenly destroyed?'
89
                    );
90
                }
91
            }
92
            case isset($config['wrap']):
93
                array_unshift($arguments, $config['method']);
94
                return call_user_func_array(array($this, $config['wrap']), $arguments);
95
96
                case isset($config['function']):
97
                return $config['function']($this, $arguments);
98
99
                default:
100
                throw new BadMethodCallException(
101
                    "Object->__call(): extra method $method is invalid on $class:"
102
                        . var_export($config, true)
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);
0 ignored issues
show
Documentation introduced by
$method is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
149
    }
150
151
    /**
152
     * Get meta-data details on a named method
153
     *
154
     * @param array $method
155
     * @return array List of custom method details, if defined for this method
156
     */
157
    protected function getExtraMethodConfig($method)
158
    {
159
        $class = get_class($this);
160
        if (isset(self::$extra_methods[$class][strtolower($method)])) {
161
            return self::$extra_methods[$class][strtolower($method)];
162
        }
163
        return 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
171
     */
172
    public function allMethodNames($custom = false)
173
    {
174
        $class = get_class($this);
175 View Code Duplication
        if (!isset(self::$built_in_methods[$class])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
176
            self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
177
        }
178
179
        if ($custom && isset(self::$extra_methods[$class])) {
180
            return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
181
        } else {
182
            return self::$built_in_methods[$class];
183
        }
184
    }
185
186
    /**
187
     * @param object $extension
188
     * @return array
189
     */
190
    protected function findMethodsFromExtension($extension)
191
    {
192
        if (method_exists($extension, 'allMethodNames')) {
193
            if ($extension instanceof Extension) {
194
                $extension->setOwner($this);
195
            }
196
            $methods = $extension->allMethodNames(true);
197
            if ($extension instanceof Extension) {
198
                $extension->clearOwner();
199
            }
200
        } else {
201 View Code Duplication
            if (!isset(self::$built_in_methods[$extension->class])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
202
                self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
203
            }
204
            $methods = self::$built_in_methods[$extension->class];
205
        }
206
207
        return $methods;
208
    }
209
210
    /**
211
     * Add all the methods from an object property (which is an {@link Extension}) to this object.
212
     *
213
     * @param string $property the property name
214
     * @param string|int $index an index to use if the property is an array
215
     * @throws InvalidArgumentException
216
     */
217
    protected function addMethodsFrom($property, $index = null)
218
    {
219
        $class = get_class($this);
220
        $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
221
222
        if (!$extension) {
223
            throw new InvalidArgumentException(
224
                "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
225
            );
226
        }
227
228
        $methods = $this->findMethodsFromExtension($extension);
229
        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...
230
            $methodInfo = array(
231
                'property' => $property,
232
                'index'    => $index,
233
                'callSetOwnerFirst' => $extension instanceof Extension,
234
            );
235
236
            $newMethods = array_fill_keys($methods, $methodInfo);
237
238
            if (isset(self::$extra_methods[$class])) {
239
                self::$extra_methods[$class] =
240
                    array_merge(self::$extra_methods[$class], $newMethods);
241
            } else {
242
                self::$extra_methods[$class] = $newMethods;
243
            }
244
        }
245
    }
246
247
    /**
248
     * Add all the methods from an object property (which is an {@link Extension}) to this object.
249
     *
250
     * @param string $property the property name
251
     * @param string|int $index an index to use if the property is an array
252
     */
253
    protected function removeMethodsFrom($property, $index = null)
254
    {
255
        $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
256
        $class = get_class($this);
257
258
        if (!$extension) {
259
            throw new InvalidArgumentException(
260
                "Object->removeMethodsFrom(): could not remove methods from {$class}->{$property}[$index]"
261
            );
262
        }
263
264
        $methods = $this->findMethodsFromExtension($extension);
265
        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...
266
            foreach ($methods as $method) {
267
                $methodInfo = self::$extra_methods[$class][$method];
268
269
                if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
270
                    unset(self::$extra_methods[$class][$method]);
271
                }
272
            }
273
274
            if (empty(self::$extra_methods[$class])) {
275
                unset(self::$extra_methods[$class]);
276
            }
277
        }
278
    }
279
280
    /**
281
     * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
282
     * can be wrapped to generateThumbnail(x)
283
     *
284
     * @param string $method the method name to wrap
285
     * @param string $wrap the method name to wrap to
286
     */
287
    protected function addWrapperMethod($method, $wrap)
288
    {
289
        $class = get_class($this);
290
        self::$extra_methods[$class][strtolower($method)] = array (
291
            'wrap'   => $wrap,
292
            'method' => $method
293
        );
294
    }
295
}
296