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
|
|
|
} |
66
|
|
|
case isset($config['property']) : { |
|
|
|
|
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']): { |
|
|
|
|
93
|
|
|
array_unshift($arguments, $config['method']); |
94
|
|
|
$wrapped = $config['wrap']; |
95
|
|
|
return $this->$wrapped(...$arguments); |
96
|
|
|
} |
97
|
|
|
case isset($config['function']): { |
|
|
|
|
98
|
|
|
return $config['function']($this, $arguments); |
99
|
|
|
} |
100
|
|
|
default: { |
|
|
|
|
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); |
|
|
|
|
228
|
|
|
} finally { |
229
|
|
|
$object->clearOwner(); |
230
|
|
|
} |
231
|
|
|
} else { |
232
|
|
|
$methods = $object->allMethodNames(true); |
233
|
|
|
} |
234
|
|
|
return $methods; |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.