Test Failed
Push — 3.x ( 9f26b5...73d2af )
by Jeroen
94:36
created

HandlersService::call()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 51
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 17.3317

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 34
c 1
b 0
f 0
nc 12
nop 3
dl 0
loc 51
ccs 16
cts 24
cp 0.6667
crap 17.3317
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Elgg;
4
5
use Elgg\Di\DiContainer;
6
use Elgg\HooksRegistrationService\Event as HrsEvent;
7
use Elgg\HooksRegistrationService\Hook as HrsHook;
8
9
/**
10
 * Helpers for providing callable-based APIs
11
 *
12
 * getType() uses code from Zend\Code\Reflection\ParameterReflection::detectType.
13
 * https://github.com/zendframework/zend-code/blob/master/src/Reflection/ParameterReflection.php
14
 *
15
 * @copyright 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
16
 * @license   http://framework.zend.com/license/new-bsd New BSD License
17
 *
18
 * @internal
19
 */
20
class HandlersService {
21
22
	/**
23
	 * Keeps track of already reported deprecated arguments callback messages
24
	 *
25
	 * @var array
26
	 */
27
	private $deprecated_args_msgs = [];
28
	
29
	/**
30
	 * Call the handler with the hook/event object
31 1089
	 *
32 1089
	 * @param callable $callable Callable
33
	 * @param mixed    $object   Event object
34 1089
	 * @param array    $args     Arguments for legacy events/hooks
35 1089
	 *
36 3
	 * @return array [success, result, object]
37 3
	 */
38 3
	public function call($callable, $object, $args) {
39 3
		$original = $callable;
40
41 3
		$callable = $this->resolveCallable($callable);
42
		if (!is_callable($callable)) {
43
			$type = is_string($object) ? $object : $object::EVENT_TYPE;
44 1087
			$description = $type . " [{$args[0]}, {$args[1]}]";
45 1087
			$msg = "Handler for $description is not callable: " . $this->describeCallable($original);
46 663
			_elgg_services()->logger->warning($msg);
47 484
48 484
			return [false, null, $object];
49 277
		}
50 277
51
		$use_object = $this->acceptsObject($callable);
52 210
		if ($use_object) {
53 210
			if (is_string($object)) {
54 210
				switch ($object) {
55
					case 'hook' :
56
						$object = new HrsHook(elgg(), $args[0], $args[1], $args[2], $args[3]);
57
						break;
58
59
					case 'event' :
60
						$object = new HrsEvent(elgg(), $args[0], $args[1], $args[2]);
61
						break;
62
63
					case 'middleware' :
64 663
					case 'controller' :
65
					case 'action' :
66
						$object = new Request(elgg(), $args[0]);
67 888
						break;
68
				}
69
			}
70 1075
71
			$result = call_user_func($callable, $object);
72
		} else {
73
			// legacy arguments
74
			if ($this->getParamTypeForCallable($callable) !== null) {
75
				$described_callback = $this->describeCallable($callable);
76
				$msg = "Using legacy style hook and event callback arguments is deprecated. Used by: {$described_callback} for [{$args[0]}, {$args[1]}].";
77
				
78
				$msg_hash = md5($msg);
79
				if (!in_array($msg_hash, $this->deprecated_args_msgs)) {
80
					elgg_deprecated_notice($msg, "3.1");
81
					$this->deprecated_args_msgs[] = $msg_hash;
82
				}
83
			}
84
			
85
			$result = call_user_func_array($callable, $args);
86
		}
87
88
		return [true, $result, $object];
89
	}
90
91
	/**
92
	 * Test is callback is callable
93
	 * Unlike is_callable(), this function also tests invokable classes
94 1091
	 *
95 1091
	 * @see is_callable()
96 767
	 *
97 11
	 * @param mixed $callback Callable
98
	 * @return bool
99
	 */
100 762
	public function isCallable($callback) {
101
		$callback = $this->resolveCallable($callback);
102
		return $callback && is_callable($callback);
103 669
	}
104 364
105
	/**
106 454
	 * Get the reflection interface for a callable
107 271
	 *
108
	 * @param callable $callable Callable
109 250
	 *
110 250
	 * @return \ReflectionFunctionAbstract
111
	 */
112
	public function getReflector($callable) {
113
		if (is_string($callable)) {
114
			if (false !== strpos($callable, '::')) {
115
				$callable = explode('::', $callable);
116
			} else {
117
				// function
118
				return new \ReflectionFunction($callable);
119
			}
120
		}
121
		if (is_array($callable)) {
122
			return new \ReflectionMethod($callable[0], $callable[1]);
123 1089
		}
124 1089
		if ($callable instanceof \Closure) {
125 973
			return new \ReflectionFunction($callable);
126
		}
127
		if (is_object($callable)) {
128 250
			return new \ReflectionMethod($callable, '__invoke');
129 250
		}
130 250
131
		throw new \InvalidArgumentException('invalid $callable');
132 247
	}
133
134
	/**
135 250
	 * Resolve a callable, possibly instantiating a class name
136
	 *
137
	 * @param callable|string $callable Callable or class name
138
	 *
139
	 * @return callable|null
140
	 */
141
	private function resolveCallable($callable) {
142
		if (is_callable($callable)) {
143
			return $callable;
144
		}
145 1087
146
		if (is_string($callable)
147 1087
			&& preg_match(DiContainer::CLASS_NAME_PATTERN_53, $callable)
148 1087
			&& class_exists($callable)) {
149
			// @todo Eventually a more advanced DIC could auto-inject dependencies
150 663
			$callable = new $callable;
151
		}
152
153 888
		return is_callable($callable) ? $callable : null;
154
	}
155
156
	/**
157
	 * Should we pass this callable a Hook/Event object instead of the 2.0 arguments?
158
	 *
159
	 * @param callable $callable Callable
160
	 *
161
	 * @return bool
162
	 */
163
	private function acceptsObject($callable) {
164 1091
		// note: caching string callables didn't help any
165 1091
		$type = (string) $this->getParamTypeForCallable($callable);
166 1091
		if (0 === strpos($type, 'Elgg\\')) {
167 180
			// probably right. We can just assume and let PHP handle it
168
			return true;
169
		}
170 1064
171
		return false;
172
	}
173
174
	/**
175
	 * Get the type for a parameter of a callable
176
	 *
177
	 * @param callable $callable Callable
178
	 * @param int      $index    Index of argument
179
	 *
180 1064
	 * @return null|string Empty string = no type, null = no parameter
181
	 */
182
	public function getParamTypeForCallable($callable, $index = 0) {
183
		$params = $this->getReflector($callable)->getParameters();
184 1064
		if (!isset($params[$index])) {
185 1064
			return null;
186 1064
		}
187 4
188
		return $this->getType($params[$index]);
189
	}
190
191 1064
	/**
192
	 * Get the type of a parameter
193
	 *
194
	 * @param \ReflectionParameter $param Parameter
195
	 *
196 1064
	 * @return string
197
	 */
198
	public function getType(\ReflectionParameter $param) {
199
		// @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
200
		// @license   http://framework.zend.com/license/new-bsd New BSD License
201 1064
202 1064
		if (method_exists($param, 'getType')
203 667
				&& ($type = $param->getType())
204
				&& $type->isBuiltin()) {
205
			return (string) $type;
206
		}
207 847
208
		// can be dropped when dropping PHP7 support:
209
		if ($param->isArray()) {
210
			return 'array';
211
		}
212
213
		// can be dropped when dropping PHP7 support:
214
		if ($param->isCallable()) {
215
			return 'callable';
216
		}
217
218
		// ReflectionParameter::__toString() doesn't require loading class
219 7
		if (preg_match('~\[\s\<\w+?>\s([\S]+)~s', (string) $param, $m)) {
220 7
			if ($m[1][0] !== '$') {
221 5
				return $m[1];
222
			}
223 5
		}
224 5
225
		return '';
226
	}
227 5
228
	/**
229 3
	 * Get a string description of a callback
230 3
	 *
231 3
	 * E.g. "function_name", "Static::method", "(ClassName)->method", "(Closure path/to/file.php:23)"
232 3
	 *
233
	 * @param mixed  $callable  Callable
234 3
	 * @param string $file_root If provided, it will be removed from the beginning of file names
235 3
	 * @return string
236
	 */
237
	public function describeCallable($callable, $file_root = '') {
238 3
		if (is_string($callable)) {
239
			return $callable;
240
		}
241
		if (is_array($callable) && array_keys($callable) === [0, 1] && is_string($callable[1])) {
242
			if (is_string($callable[0])) {
243
				return "{$callable[0]}::{$callable[1]}";
244
			}
245
			return "(" . get_class($callable[0]) . ")->{$callable[1]}";
246
		}
247
		if ($callable instanceof \Closure) {
248
			$ref = new \ReflectionFunction($callable);
249
			$file = $ref->getFileName();
250
			$line = $ref->getStartLine();
251
252
			if ($file_root && 0 === strpos($file, $file_root)) {
253
				$file = substr($file, strlen($file_root));
254
			}
255
256
			return "(Closure {$file}:{$line})";
257
		}
258
		if (is_object($callable)) {
259
			return "(" . get_class($callable) . ")->__invoke()";
260
		}
261
		return print_r($callable, true);
262
	}
263
264
	/**
265
	 * Get a string that uniquely identifies a callback across requests (for caching)
266
	 *
267
	 * @param callable $callable Callable
268
	 *
269
	 * @return string Empty if cannot uniquely identify this callable
270
	 */
271
	public function fingerprintCallable($callable) {
272
		if (is_string($callable)) {
273
			return $callable;
274
		}
275
		if (is_array($callable)) {
276
			if (is_string($callable[0])) {
277
				return "{$callable[0]}::{$callable[1]}";
278
			}
279
			return get_class($callable[0]) . "::{$callable[1]}";
280
		}
281
		if ($callable instanceof \Closure) {
282
			return '';
283
		}
284
		if (is_object($callable)) {
285
			return get_class($callable) . "::__invoke";
286
		}
287
		// this should not happen
288
		return '';
289
	}
290
}
291