Completed
Push — master ( 5776a0...605463 )
by Daniel
23s
created

CustomMethods::getExtraMethodConfig()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 9.4285
c 1
b 1
f 0
1
<?php
2
3
namespace SilverStripe\Framework\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
     * Custom method sources
15
     *
16
     * @var array
17
     */
18
	protected static $extra_methods = array();
19
20
	/**
21
	 * Name of methods to invoke by defineMethods for this instance
22
	 *
23
	 * @var array
24
	 */
25
	protected $extra_method_registers = array();
26
27
    /**
28
     * Non-custom methods
29
     *
30
     * @var array
31
     */
32
    protected static $built_in_methods = array();
33
34
    /**
35
     * Attempts to locate and call a method dynamically added to a class at runtime if a default cannot be located
36
     *
37
     * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
38
     * {@link Object::addWrapperMethod()}
39
     *
40
     * @param string $method
41
     * @param array $arguments
42
     * @return mixed
43
     * @throws BadMethodCallException
44
     */
45
	public function __call($method, $arguments) {
46
		// If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
47
		// call, then we should rebuild it.
48
        $class = get_class($this);
49
		if(!array_key_exists($class, self::$extra_methods)) {
50
			$this->defineMethods();
51
		}
52
53
		$config = $this->getExtraMethodConfig($method);
54
		if(empty($config)) {
55
			throw new BadMethodCallException(
56
				"Object->__call(): the method '$method' does not exist on '$class'"
57
			);
58
		}
59
60
		switch(true) {
61
			case isset($config['property']) : {
0 ignored issues
show
Coding Style introduced by
CASE statements must 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.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
62
				$obj = $config['index'] !== null ?
63
					$this->{$config['property']}[$config['index']] :
64
					$this->{$config['property']};
65
66
				if ($obj) {
67
					if (!empty($config['callSetOwnerFirst'])) {
68
						$obj->setOwner($this);
69
					}
70
					$retVal = call_user_func_array(array($obj, $method), $arguments);
71
					if (!empty($config['callSetOwnerFirst'])) {
72
						$obj->clearOwner();
73
					}
74
					return $retVal;
75
				}
76
77
				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...
78
					throw new BadMethodCallException(
79
						"Object->__call(): attempt to call $method on a destroyed $class object"
80
					);
81
				} else {
82
					throw new BadMethodCallException(
83
						"Object->__call(): $class cannot pass control to $config[property]($config[index])."
84
						. ' Perhaps this object was mistakenly destroyed?'
85
					);
86
				}
87
			}
88
			case isset($config['wrap']) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
89
				array_unshift($arguments, $config['method']);
90
				return call_user_func_array(array($this, $config['wrap']), $arguments);
91
92
			case isset($config['function']) :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
93
				return $config['function']($this, $arguments);
94
95
			default :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a DEFAULT statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in the default statement.

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...
96
				throw new BadMethodCallException(
97
					"Object->__call(): extra method $method is invalid on $class:"
98
						. var_export($config, true)
99
				);
100
		}
101
	}
102
103
    /**
104
	 * Adds any methods from {@link Extension} instances attached to this object.
105
	 * All these methods can then be called directly on the instance (transparently
106
	 * mapped through {@link __call()}), or called explicitly through {@link extend()}.
107
	 *
108
	 * @uses addMethodsFrom()
109
	 */
110
	protected function defineMethods() {
111
		// Define from all registered callbacks
112
		foreach($this->extra_method_registers as $callback) {
113
			call_user_func($callback);
114
		}
115
	}
116
117
	/**
118
	 * Register an callback to invoke that defines extra methods
119
	 *
120
	 * @param string $name
121
	 * @param callable $callback
122
	 */
123
	protected function registerExtraMethodCallback($name, $callback) {
124
		if(!isset($this->extra_method_registers[$name])) {
125
			$this->extra_method_registers[$name] = $callback;
126
		}
127
	}
128
129
	// --------------------------------------------------------------------------------------------------------------
130
131
	/**
132
	 * Return TRUE if a method exists on this object
133
	 *
134
	 * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
135
	 * extensions
136
	 *
137
	 * @param string $method
138
	 * @return bool
139
	 */
140
	public function hasMethod($method) {
141
		return method_exists($this, $method) || $this->getExtraMethodConfig($method);
142
	}
143
144
	/**
145
	 * Get meta-data details on a named method
146
	 *
147
	 * @param array $method
148
	 * @return array List of custom method details, if defined for this method
149
	 */
150
	protected function getExtraMethodConfig($method) {
151
		$class = get_class($this);
152
		if(isset(self::$extra_methods[$class][strtolower($method)])) {
153
			return self::$extra_methods[$class][strtolower($method)];
154
		}
155
		return null;
156
	}
157
158
	/**
159
	 * Return the names of all the methods available on this object
160
	 *
161
	 * @param bool $custom include methods added dynamically at runtime
162
	 * @return array
163
	 */
164
	public function allMethodNames($custom = false) {
165
		$class = get_class($this);
166
		if(!isset(self::$built_in_methods[$class])) {
167
			self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
168
		}
169
170
		if($custom && isset(self::$extra_methods[$class])) {
171
			return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
172
		} else {
173
			return self::$built_in_methods[$class];
174
		}
175
	}
176
177
	/**
178
	 * @param object $extension
179
	 * @return array
180
	 */
181
	protected function findMethodsFromExtension($extension) {
182
		if (method_exists($extension, 'allMethodNames')) {
183
			if ($extension instanceof \Extension) $extension->setOwner($this);
184
			$methods = $extension->allMethodNames(true);
185
			if ($extension instanceof \Extension) $extension->clearOwner();
186
		} else {
187
			if (!isset(self::$built_in_methods[$extension->class])) {
188
				self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
189
			}
190
			$methods = self::$built_in_methods[$extension->class];
191
		}
192
193
		return $methods;
194
	}
195
196
	/**
197
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
198
	 *
199
	 * @param string $property the property name
200
	 * @param string|int $index an index to use if the property is an array
201
	 * @throws InvalidArgumentException
202
	 */
203
	protected function addMethodsFrom($property, $index = null) {
204
		$class = get_class($this);
205
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
206
207
		if (!$extension) {
208
			throw new InvalidArgumentException(
209
				"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
210
			);
211
		}
212
213
		$methods = $this->findMethodsFromExtension($extension);
214
		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...
215
			$methodInfo = array(
216
				'property' => $property,
217
				'index'    => $index,
218
				'callSetOwnerFirst' => $extension instanceof \Extension,
219
			);
220
221
			$newMethods = array_fill_keys($methods, $methodInfo);
222
223
			if(isset(self::$extra_methods[$class])) {
224
				self::$extra_methods[$class] =
225
					array_merge(self::$extra_methods[$class], $newMethods);
226
			} else {
227
				self::$extra_methods[$class] = $newMethods;
228
			}
229
		}
230
	}
231
232
	/**
233
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
234
	 *
235
	 * @param string $property the property name
236
	 * @param string|int $index an index to use if the property is an array
237
	 */
238
	protected function removeMethodsFrom($property, $index = null) {
239
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
240
241
		if (!$extension) {
242
			throw new InvalidArgumentException(
243
				"Object->removeMethodsFrom(): could not remove methods from {$this->class}->{$property}[$index]"
0 ignored issues
show
Bug introduced by
The property class 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...
244
			);
245
		}
246
247
		$methods = $this->findMethodsFromExtension($extension);
248
		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...
249
			foreach ($methods as $method) {
250
				$methodInfo = self::$extra_methods[$this->class][$method];
251
252
				if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
253
					unset(self::$extra_methods[$this->class][$method]);
254
				}
255
			}
256
257
			if (empty(self::$extra_methods[$this->class])) {
258
				unset(self::$extra_methods[$this->class]);
259
			}
260
		}
261
	}
262
263
	/**
264
	 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
265
	 * can be wrapped to generateThumbnail(x)
266
	 *
267
	 * @param string $method the method name to wrap
268
	 * @param string $wrap the method name to wrap to
269
	 */
270
	protected function addWrapperMethod($method, $wrap) {
271
		$class = get_class($this);
272
		self::$extra_methods[$class][strtolower($method)] = array (
273
			'wrap'   => $wrap,
274
			'method' => $method
275
		);
276
	}
277
278
	/**
279
	 * Add an extra method using raw PHP code passed as a string
280
	 *
281
	 * @param string $method the method name
282
	 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
283
	 *        by using $obj. Note that you cannot call protected methods, as the method is actually an external
284
	 *        function
285
	 */
286
	protected function createMethod($method, $code) {
287
		$class = get_class($this);
288
		self::$extra_methods[$class][strtolower($method)] = array (
289
			'function' => create_function('$obj, $args', $code)
0 ignored issues
show
Security Best Practice introduced by
The use of create_function is highly discouraged, better use a closure.

create_function can pose a great security vulnerability as it is similar to eval, and could be used for arbitrary code execution. We highly recommend to use a closure instead.

// Instead of
$function = create_function('$a, $b', 'return $a + $b');

// Better use
$function = function($a, $b) { return $a + $b; }
Loading history...
290
		);
291
	}
292
}
293