-
Notifications
You must be signed in to change notification settings - Fork 814
Expand file tree
/
Copy pathCustomMethods.php
More file actions
350 lines (315 loc) · 11.3 KB
/
CustomMethods.php
File metadata and controls
350 lines (315 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<?php
namespace SilverStripe\Core;
use BadMethodCallException;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;
/**
* Allows an object to declare a set of custom methods
*/
trait CustomMethods
{
/**
* Custom method sources
*
* @var array Array of class names (lowercase) to list of methods.
* The list of methods will have lowercase keys. Each value in this array
* can be a callable, array, or string callback
*/
protected static $extra_methods = [];
/**
* Name of methods to invoke by defineMethods for this instance
*
* @var array
*/
protected $extra_method_registers = [];
/**
* Non-custom public methods.
*
* @var array Array of class names (lowercase) to list of methods.
* The list of methods will have lowercase keys and correct-case values.
*/
protected static $built_in_methods = [];
/**
* Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
*
* You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
* {@link Object::addWrapperMethod()}
*
* @param string $method
* @param array $arguments
* @return mixed
* @throws BadMethodCallException
*/
public function __call($method, $arguments)
{
// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
// call, then we should rebuild it.
$class = static::class;
$config = $this->getExtraMethodConfig($method);
if (empty($config)) {
throw new BadMethodCallException(
"Object->__call(): the method '$method' does not exist on '$class'"
);
}
switch (true) {
case isset($config['callback']): {
return $config['callback']($this, $arguments);
}
case isset($config['property']) : {
$property = $config['property'];
$index = $config['index'];
$obj = $index !== null ?
$this->{$property}[$index] :
$this->{$property};
if (!$obj) {
throw new BadMethodCallException(
"Object->__call(): {$class} cannot pass control to {$property}({$index})."
. ' Perhaps this object was mistakenly destroyed?'
);
}
// Call on object
try {
if ($obj instanceof Extension) {
$obj->setOwner($this);
}
return $obj->$method(...$arguments);
} finally {
if ($obj instanceof Extension) {
$obj->clearOwner();
}
}
}
case isset($config['wrap']): {
array_unshift($arguments, $config['method']);
$wrapped = $config['wrap'];
return $this->$wrapped(...$arguments);
}
case isset($config['function']): {
return $config['function']($this, $arguments);
}
default: {
throw new BadMethodCallException(
"Object->__call(): extra method $method is invalid on $class:"
. var_export($config, true)
);
}
}
}
/**
* Adds any methods from {@link Extension} instances attached to this object.
* All these methods can then be called directly on the instance (transparently
* mapped through {@link __call()}), or called explicitly through {@link extend()}.
*
* @uses addMethodsFrom()
*/
protected function defineMethods()
{
// Define from all registered callbacks
foreach ($this->extra_method_registers as $callback) {
call_user_func($callback);
}
}
/**
* Register an callback to invoke that defines extra methods
*
* @param string $name
* @param callable $callback
*/
protected function registerExtraMethodCallback($name, $callback)
{
if (!isset($this->extra_method_registers[$name])) {
$this->extra_method_registers[$name] = $callback;
}
}
// --------------------------------------------------------------------------------------------------------------
/**
* Return TRUE if a method exists on this object
*
* This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
* extensions
*
* @param string $method
* @return bool
*/
public function hasMethod($method)
{
return method_exists($this, $method ?? '') || $this->hasCustomMethod($method);
}
/**
* Determines if a custom method with this name is defined.
*/
protected function hasCustomMethod($method): bool
{
return $this->getExtraMethodConfig($method) !== null;
}
/**
* Get meta-data details on a named method
*
* @param string $method
* @return array List of custom method details, if defined for this method
*/
protected function getExtraMethodConfig($method)
{
if (empty($method)) {
return null;
}
// Lazy define methods
$lowerClass = strtolower(static::class);
if (!isset(self::class::$extra_methods[$lowerClass])) {
$this->defineMethods();
}
return self::class::$extra_methods[$lowerClass][strtolower($method)] ?? null;
}
/**
* Return the names of all the methods available on this object
*
* @param bool $custom include methods added dynamically at runtime
* @return array Map of method names with lowercase keys
*/
public function allMethodNames($custom = false)
{
$methods = static::findBuiltInMethods();
// Query extra methods
$lowerClass = strtolower(static::class);
if ($custom && isset(self::class::$extra_methods[$lowerClass])) {
$methods = array_merge(self::class::$extra_methods[$lowerClass], $methods);
}
return $methods;
}
/**
* Get all public built in methods for this class
*
* @param string|object $class Class or instance to query methods from (defaults to static::class)
* @return array Map of methods with lowercase key name
*/
protected static function findBuiltInMethods($class = null)
{
$class = is_object($class) ? get_class($class) : ($class ?: static::class);
$lowerClass = strtolower($class);
if (isset(self::class::$built_in_methods[$lowerClass])) {
return self::class::$built_in_methods[$lowerClass];
}
// Build new list
$reflection = new ReflectionClass($class);
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
self::class::$built_in_methods[$lowerClass] = [];
foreach ($methods as $method) {
$name = $method->getName();
self::class::$built_in_methods[$lowerClass][strtolower($name)] = $name;
}
return self::class::$built_in_methods[$lowerClass];
}
/**
* Find all methods on the given object.
*
* @param object $object
* @return array
*/
protected function findMethodsFrom($object)
{
// Respect "allMethodNames"
if (method_exists($object, 'allMethodNames')) {
if ($object instanceof Extension) {
try {
$object->setOwner($this);
$methods = $object->allMethodNames(true);
} finally {
$object->clearOwner();
}
} else {
$methods = $object->allMethodNames(true);
}
return $methods;
}
// Get methods
return static::findBuiltInMethods($object);
}
/**
* Add all the methods from an object property.
*
* @param string $property the property name
* @param string|int $index an index to use if the property is an array
* @throws InvalidArgumentException
*/
protected function addMethodsFrom($property, $index = null)
{
$class = static::class;
$object = ($index !== null) ? $this->{$property}[$index] : $this->$property;
if (!$object) {
throw new InvalidArgumentException(
"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
);
}
$methods = $this->findMethodsFrom($object);
if (!$methods) {
return;
}
$methodInfo = [
'property' => $property,
'index' => $index,
];
$newMethods = array_fill_keys(array_keys($methods), $methodInfo);
// Merge with extra_methods
$lowerClass = strtolower($class);
if (isset(self::class::$extra_methods[$lowerClass])) {
self::class::$extra_methods[$lowerClass] = array_merge(self::class::$extra_methods[$lowerClass], $newMethods);
} else {
self::class::$extra_methods[$lowerClass] = $newMethods;
}
}
/**
* Add all the methods from an object property (which is an {@link Extension}) to this object.
*
* @param string $property the property name
* @param string|int $index an index to use if the property is an array
*/
protected function removeMethodsFrom($property, $index = null)
{
$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
$class = static::class;
if (!$extension) {
throw new InvalidArgumentException(
"Object->removeMethodsFrom(): could not remove methods from {$class}->{$property}[$index]"
);
}
$lowerClass = strtolower($class);
if (!isset(self::class::$extra_methods[$lowerClass])) {
return;
}
$methods = $this->findMethodsFrom($extension);
// Unset by key
self::class::$extra_methods[$lowerClass] = array_diff_key(self::class::$extra_methods[$lowerClass], $methods);
// Clear empty list
if (empty(self::class::$extra_methods[$lowerClass])) {
unset(self::class::$extra_methods[$lowerClass]);
}
}
/**
* Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
* can be wrapped to generateThumbnail(x)
*
* @param string $method the method name to wrap
* @param string $wrap the method name to wrap to
*/
protected function addWrapperMethod($method, $wrap)
{
self::class::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'wrap' => $wrap,
'method' => $method
];
}
/**
* Add callback as a method.
*
* @param string $method Name of method
* @param callable $callback Callback to invoke.
* Note: $this is passed as first parameter to this callback and then $args as array
*/
protected function addCallbackMethod($method, $callback)
{
self::class::$extra_methods[strtolower(static::class)][strtolower($method)] = [
'callback' => $callback,
];
}
}