Completed
Push — master ( 33496d...9e3f76 )
by Daniel
12:54
created

CustomMethods::removeMethodsFrom()   C

Complexity

Conditions 8
Paths 16

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 13
nc 16
nop 2
dl 0
loc 24
rs 5.7377
c 0
b 0
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
		// Validate method being invked
54
		$method = strtolower($method);
55
		if(!isset(self::$extra_methods[$class][$method])) {
56
			// Please do not change the exception code number below.
57
			$class = get_class($this);
58
			throw new BadMethodCallException("Object->__call(): the method '$method' does not exist on '$class'", 2175);
59
		}
60
61
		$config = self::$extra_methods[$class][$method];
62
63
		switch(true) {
64
			case isset($config['property']) :
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...
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
65
				$obj = $config['index'] !== null ?
66
					$this->{$config['property']}[$config['index']] :
67
					$this->{$config['property']};
68
69
				if($obj) {
70
					if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
71
					$retVal = call_user_func_array(array($obj, $method), $arguments);
72
					if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
73
					return $retVal;
74
				}
75
76
				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...
77
					throw new BadMethodCallException(
78
						"Object->__call(): attempt to call $method on a destroyed $class object"
79
					);
80
				} else {
81
					throw new BadMethodCallException(
82
						"Object->__call(): $class cannot pass control to $config[property]($config[index])."
83
							. ' Perhaps this object was mistakenly destroyed?'
84
					);
85
				}
86
87
			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...
88
				array_unshift($arguments, $config['method']);
89
				return call_user_func_array(array($this, $config['wrap']), $arguments);
90
91
			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...
92
				return $config['function']($this, $arguments);
93
94
			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...
95
				throw new BadMethodCallException(
96
					"Object->__call(): extra method $method is invalid on $class:"
97
						. var_export($config, true)
98
				);
99
		}
100
	}
101
102
    /**
103
	 * Adds any methods from {@link Extension} instances attached to this object.
104
	 * All these methods can then be called directly on the instance (transparently
105
	 * mapped through {@link __call()}), or called explicitly through {@link extend()}.
106
	 *
107
	 * @uses addMethodsFrom()
108
	 */
109
	protected function defineMethods() {
110
		$class = get_class($this);
111
112
		// Define from all registered callbacks
113
		foreach($this->extra_method_registers as $callback) {
114
			call_user_func($callback);
115
		}
116
	}
117
118
	/**
119
	 * Register an callback to invoke that defines extra methods
120
	 *
121
	 * @param string $name
122
	 * @param callable $callback
123
	 */
124
	protected function registerExtraMethodCallback($name, $callback) {
125
		if(!isset($this->extra_method_registers[$name])) {
126
			$this->extra_method_registers[$name] = $callback;
127
		}
128
	}
129
130
	// --------------------------------------------------------------------------------------------------------------
131
132
	/**
133
	 * Return TRUE if a method exists on this object
134
	 *
135
	 * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
136
	 * extensions
137
	 *
138
	 * @param string $method
139
	 * @return bool
140
	 */
141
	public function hasMethod($method) {
142
		$class = get_class($this);
143
		return method_exists($this, $method) || isset(self::$extra_methods[$class][strtolower($method)]);
144
	}
145
146
	/**
147
	 * Return the names of all the methods available on this object
148
	 *
149
	 * @param bool $custom include methods added dynamically at runtime
150
	 * @return array
151
	 */
152
	public function allMethodNames($custom = false) {
153
		$class = get_class($this);
154
		if(!isset(self::$built_in_methods[$class])) {
155
			self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
156
		}
157
158
		if($custom && isset(self::$extra_methods[$class])) {
159
			return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
160
		} else {
161
			return self::$built_in_methods[$class];
162
		}
163
	}
164
165
	/**
166
	 * @param object $extension
167
	 * @return array
168
	 */
169
	protected function findMethodsFromExtension($extension) {
170
		if (method_exists($extension, 'allMethodNames')) {
171
			if ($extension instanceof \Extension) $extension->setOwner($this);
172
			$methods = $extension->allMethodNames(true);
173
			if ($extension instanceof \Extension) $extension->clearOwner();
174
		} else {
175
			if (!isset(self::$built_in_methods[$extension->class])) {
176
				self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
177
			}
178
			$methods = self::$built_in_methods[$extension->class];
179
		}
180
181
		return $methods;
182
	}
183
184
	/**
185
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
186
	 *
187
	 * @param string $property the property name
188
	 * @param string|int $index an index to use if the property is an array
189
	 * @throws InvalidArgumentException
190
	 */
191
	protected function addMethodsFrom($property, $index = null) {
192
		$class = get_class($this);
193
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
194
195
		if (!$extension) {
196
			throw new InvalidArgumentException(
197
				"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
198
			);
199
		}
200
201
		$methods = $this->findMethodsFromExtension($extension);
202
		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...
203
			$methodInfo = array(
204
				'property' => $property,
205
				'index'    => $index,
206
				'callSetOwnerFirst' => $extension instanceof \Extension,
207
			);
208
209
			$newMethods = array_fill_keys($methods, $methodInfo);
210
211
			if(isset(self::$extra_methods[$class])) {
212
				self::$extra_methods[$class] =
213
					array_merge(self::$extra_methods[$class], $newMethods);
214
			} else {
215
				self::$extra_methods[$class] = $newMethods;
216
			}
217
		}
218
	}
219
220
	/**
221
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
222
	 *
223
	 * @param string $property the property name
224
	 * @param string|int $index an index to use if the property is an array
225
	 */
226
	protected function removeMethodsFrom($property, $index = null) {
227
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
228
229
		if (!$extension) {
230
			throw new InvalidArgumentException(
231
				"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...
232
			);
233
		}
234
235
		$methods = $this->findMethodsFromExtension($extension);
236
		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...
237
			foreach ($methods as $method) {
238
				$methodInfo = self::$extra_methods[$this->class][$method];
239
240
				if ($methodInfo['property'] === $property && $methodInfo['index'] === $index) {
241
					unset(self::$extra_methods[$this->class][$method]);
242
				}
243
			}
244
245
			if (empty(self::$extra_methods[$this->class])) {
246
				unset(self::$extra_methods[$this->class]);
247
			}
248
		}
249
	}
250
251
	/**
252
	 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
253
	 * can be wrapped to generateThumbnail(x)
254
	 *
255
	 * @param string $method the method name to wrap
256
	 * @param string $wrap the method name to wrap to
257
	 */
258
	protected function addWrapperMethod($method, $wrap) {
259
		$class = get_class($this);
260
		self::$extra_methods[$class][strtolower($method)] = array (
261
			'wrap'   => $wrap,
262
			'method' => $method
263
		);
264
	}
265
266
	/**
267
	 * Add an extra method using raw PHP code passed as a string
268
	 *
269
	 * @param string $method the method name
270
	 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
271
	 *        by using $obj. Note that you cannot call protected methods, as the method is actually an external
272
	 *        function
273
	 */
274
	protected function createMethod($method, $code) {
275
		$class = get_class($this);
276
		self::$extra_methods[$class][strtolower($method)] = array (
277
			'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...
278
		);
279
	}
280
}
281