Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

CustomMethods   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1
Metric Value
wmc 32
lcom 1
cbo 1
dl 0
loc 237
rs 9.6

8 Methods

Rating   Name   Duplication   Size   Complexity  
C __call() 0 56 11
A defineMethods() 0 8 2
A registerExtraMethodCallback() 0 5 2
A hasMethod() 0 4 2
A allMethodNames() 0 12 4
D addMethodsFrom() 0 43 9
A addWrapperMethod() 0 7 1
A createMethod() 0 6 1
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
167
	/**
168
	 * Add all the methods from an object property (which is an {@link Extension}) to this object.
169
	 *
170
	 * @param string $property the property name
171
	 * @param string|int $index an index to use if the property is an array
172
	 * @throws InvalidArgumentException
173
	 */
174
	protected function addMethodsFrom($property, $index = null) {
175
		$class = get_class($this);
176
		$extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
177
178
		if(!$extension) {
179
			throw new InvalidArgumentException (
180
				"Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
181
			);
182
		}
183
184
		if(method_exists($extension, 'allMethodNames')) {
185
			if ($extension instanceof \Extension) {
186
				$extension->setOwner($this);
187
			}
188
			$methods = $extension->allMethodNames(true);
189
			if ($extension instanceof \Extension) {
190
				$extension->clearOwner();
191
			}
192
193
		} else {
194
			if(!isset(self::$built_in_methods[$extension->class])) {
195
				self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
196
			}
197
			$methods = self::$built_in_methods[$extension->class];
198
		}
199
200
		if($methods) {
201
			$methodInfo = array(
202
				'property' => $property,
203
				'index'    => $index,
204
				'callSetOwnerFirst' => $extension instanceof \Extension,
205
			);
206
207
			$newMethods = array_fill_keys($methods, $methodInfo);
208
209
			if(isset(self::$extra_methods[$class])) {
210
				self::$extra_methods[$class] =
211
					array_merge(self::$extra_methods[$class], $newMethods);
212
			} else {
213
				self::$extra_methods[$class] = $newMethods;
214
			}
215
		}
216
	}
217
218
	/**
219
	 * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
220
	 * can be wrapped to generateThumbnail(x)
221
	 *
222
	 * @param string $method the method name to wrap
223
	 * @param string $wrap the method name to wrap to
224
	 */
225
	protected function addWrapperMethod($method, $wrap) {
226
		$class = get_class($this);
227
		self::$extra_methods[$class][strtolower($method)] = array (
228
			'wrap'   => $wrap,
229
			'method' => $method
230
		);
231
	}
232
233
	/**
234
	 * Add an extra method using raw PHP code passed as a string
235
	 *
236
	 * @param string $method the method name
237
	 * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
238
	 *        by using $obj. Note that you cannot call protected methods, as the method is actually an external
239
	 *        function
240
	 */
241
	protected function createMethod($method, $code) {
242
		$class = get_class($this);
243
		self::$extra_methods[$class][strtolower($method)] = array (
244
			'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...
245
		);
246
	}
247
}
248