Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

HandlersService::resolveCallable()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 7
nc 5
nop 1
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
ccs 8
cts 8
cp 1
crap 6
1
<?php
2
namespace Elgg;
3
4
use Elgg\Di\DiContainer;
5
use Elgg\HooksRegistrationService\Event;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Elgg\Event. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
6
use Elgg\HooksRegistrationService\Hook;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Elgg\Hook. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
8
/**
9
 * Helpers for providing callable-based APIs
10
 *
11
 * getType() uses code from Zend\Code\Reflection\ParameterReflection::detectType.
12
 * https://github.com/zendframework/zend-code/blob/master/src/Reflection/ParameterReflection.php
13
 *
14
 * @copyright 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
15
 * @license   http://framework.zend.com/license/new-bsd New BSD License
16
 *
17
 * @access private
18
 */
19
class HandlersService {
20
21
	/**
22
	 * Call the handler with the hook/event object
23
	 *
24
	 * @param callable          $callable Callable
25
	 * @param string|Hook|Event $object   Event object
26
	 * @param array             $args     Arguments for legacy events/hooks
27
	 *
28
	 * @return array [success, result, object]
29
	 */
30 704
	public function call($callable, $object, $args) {
31 704
		$original = $callable;
32
33 704
		$callable = $this->resolveCallable($callable);
34 704
		if (!is_callable($callable)) {
35 3
			$type = is_string($object) ? $object : $object::EVENT_TYPE;
36 3
			$description = $type . " [{$args[0]}, {$args[1]}]";
37 3
			$msg = "Handler for $description is not callable: " . $this->describeCallable($original);
38 3
			_elgg_services()->logger->warn($msg);
39
40 3
			return [false, null, $object];
41
		}
42
43 702
		$use_object = $this->acceptsObject($callable);
44 702
		if ($use_object) {
45 308
			if (is_string($object)) {
46 308
				if ($object === 'hook') {
47 228
					$object = new Hook(elgg(), $args[0], $args[1], $args[2], $args[3]);
48
				} else {
49 81
					$object = new Event(elgg(), $args[0], $args[1], $args[2]);
50
				}
51
			}
52
53 308
			$result = call_user_func($callable, $object);
54
		} else {
55
			// legacy arguments
56 652
			$result = call_user_func_array($callable, $args);
57
		}
58
59 701
		return [true, $result, $object];
60
	}
61
62
	/**
63
	 * Get the reflection interface for a callable
64
	 *
65
	 * @param callable $callable Callable
66
	 *
67
	 * @return \ReflectionFunctionAbstract
68
	 */
69 706
	public function getReflector($callable) {
70 706
		if (is_string($callable)) {
71 529
			if (false !== strpos($callable, '::')) {
72 11
				$callable = explode('::', $callable);
73
			} else {
74
				// function
75 524
				return new \ReflectionFunction($callable);
76
			}
77
		}
78 411
		if (is_array($callable)) {
79 249
			return new \ReflectionMethod($callable[0], $callable[1]);
80
		}
81 172
		if ($callable instanceof \Closure) {
82 160
			return new \ReflectionFunction($callable);
83
		}
84 15
		if (is_object($callable)) {
85 15
			return new \ReflectionMethod($callable, '__invoke');
86
		}
87
88
		throw new \InvalidArgumentException('invalid $callable');
89
	}
90
91
	/**
92
	 * Resolve a callable, possibly instantiating a class name
93
	 *
94
	 * @param callable|string $callable Callable or class name
95
	 *
96
	 * @return callable|null
97
	 */
98 704
	private function resolveCallable($callable) {
99 704
		if (is_callable($callable)) {
100 698
			return $callable;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $callable also could return the type string which is incompatible with the documented return type null|callable.
Loading history...
101
		}
102
103 16
		if (is_string($callable)
104 16
			&& preg_match(DiContainer::CLASS_NAME_PATTERN_53, $callable)
105 16
			&& class_exists($callable)) {
106
			// @todo Eventually a more advanced DIC could auto-inject dependencies
107 13
			$callable = new $callable;
108
		}
109
110 16
		return is_callable($callable) ? $callable : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_callable($callable) ? $callable : null also could return the type object|string which is incompatible with the documented return type null|callable.
Loading history...
111
	}
112
113
	/**
114
	 * Should we pass this callable a Hook/Event object instead of the 2.0 arguments?
115
	 *
116
	 * @param callable $callable Callable
117
	 *
118
	 * @return bool
119
	 */
120 702
	private function acceptsObject($callable) {
121
		// note: caching string callables didn't help any
122 702
		$type = (string) $this->getParamTypeForCallable($callable);
123 702
		if (0 === strpos($type, 'Elgg\\')) {
124
			// probably right. We can just assume and let PHP handle it
125 308
			return true;
126
		}
127
128 652
		return false;
129
	}
130
131
	/**
132
	 * Get the type for a parameter of a callable
133
	 *
134
	 * @param callable $callable Callable
135
	 * @param int      $index    Index of argument
136
	 *
137
	 * @return null|string Empty string = no type, null = no parameter
138
	 */
139 706
	public function getParamTypeForCallable($callable, $index = 0) {
140 706
		$params = $this->getReflector($callable)->getParameters();
141 706
		if (!isset($params[$index])) {
142 78
			return null;
143
		}
144
145 681
		return $this->getType($params[$index]);
146
	}
147
148
	/**
149
	 * Get the type of a parameter
150
	 *
151
	 * @param \ReflectionParameter $param Parameter
152
	 *
153
	 * @return string
154
	 */
155 681
	public function getType(\ReflectionParameter $param) {
156
		// @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
157
		// @license   http://framework.zend.com/license/new-bsd New BSD License
158
159 681
		if (method_exists($param, 'getType')
160 681
				&& ($type = $param->getType())
161 681
				&& $type->isBuiltin()) {
162 4
			return (string) $type;
163
		}
164
165
		// can be dropped when dropping PHP7 support:
166 681
		if ($param->isArray()) {
167
			return 'array';
168
		}
169
170
		// can be dropped when dropping PHP7 support:
171 681
		if ($param->isCallable()) {
172
			return 'callable';
173
		}
174
175
		// ReflectionParameter::__toString() doesn't require loading class
176 681
		if (preg_match('~\[\s\<\w+?>\s([\S]+)~s', (string) $param, $m)) {
177 681
			if ($m[1][0] !== '$') {
178 312
				return $m[1];
179
			}
180
		}
181
182 631
		return '';
183
	}
184
185
	/**
186
	 * Get a string description of a callback
187
	 *
188
	 * E.g. "function_name", "Static::method", "(ClassName)->method", "(Closure path/to/file.php:23)"
189
	 *
190
	 * @param mixed  $callable  Callable
191
	 * @param string $file_root If provided, it will be removed from the beginning of file names
192
	 * @return string
193
	 */
194 5
	public function describeCallable($callable, $file_root = '') {
195 5
		if (is_string($callable)) {
196 3
			return $callable;
197
		}
198 3
		if (is_array($callable) && array_keys($callable) === [0, 1] && is_string($callable[1])) {
199 3
			if (is_string($callable[0])) {
200
				return "{$callable[0]}::{$callable[1]}";
201
			}
202 3
			return "(" . get_class($callable[0]) . ")->{$callable[1]}";
203
		}
204 1
		if ($callable instanceof \Closure) {
205 1
			$ref = new \ReflectionFunction($callable);
206 1
			$file = $ref->getFileName();
207 1
			$line = $ref->getStartLine();
208
209 1
			if ($file_root && 0 === strpos($file, $file_root)) {
210 1
				$file = substr($file, strlen($file_root));
211
			}
212
213 1
			return "(Closure {$file}:{$line})";
214
		}
215
		if (is_object($callable)) {
216
			return "(" . get_class($callable) . ")->__invoke()";
217
		}
218
		return print_r($callable, true);
219
	}
220
221
	/**
222
	 * Get a string that uniquely identifies a callback across requests (for caching)
223
	 *
224
	 * @param callable $callable Callable
225
	 *
226
	 * @return string Empty if cannot uniquely identify this callable
227
	 */
228
	public function fingerprintCallable($callable) {
229
		if (is_string($callable)) {
230
			return $callable;
231
		}
232
		if (is_array($callable)) {
233
			if (is_string($callable[0])) {
234
				return "{$callable[0]}::{$callable[1]}";
235
			}
236
			return get_class($callable[0]) . "::{$callable[1]}";
237
		}
238
		if ($callable instanceof \Closure) {
239
			return '';
240
		}
241
		if (is_object($callable)) {
242
			return get_class($callable) . "::__invoke";
243
		}
244
		// this should not happen
245
		return '';
246
	}
247
}
248