Completed
Push — 3.1 ( d59679...4b8741 )
by Jeroen
62:38 queued 13s
created

engine/classes/Elgg/HandlersService.php (2 issues)

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
	 *
32
	 * @param callable $callable Callable
33
	 * @param mixed    $object   Event object
34
	 * @param array    $args     Arguments for legacy events/hooks
35
	 *
36
	 * @return array [success, result, object]
37
	 */
38 1103
	public function call($callable, $object, $args) {
39 1103
		$original = $callable;
40
41 1103
		$callable = $this->resolveCallable($callable);
42 1103
		if (!is_callable($callable)) {
43 3
			$type = is_string($object) ? $object : $object::EVENT_TYPE;
44 3
			$description = $type . " [{$args[0]}, {$args[1]}]";
45 3
			$msg = "Handler for $description is not callable: " . $this->describeCallable($original);
46 3
			_elgg_services()->logger->warning($msg);
47
48 3
			return [false, null, $object];
49
		}
50
51 1101
		$use_object = $this->acceptsObject($callable);
52 1101
		if ($use_object) {
53 1045
			if (is_string($object)) {
54 907
				switch ($object) {
55 907
					case 'hook' :
56 634
						$object = new HrsHook(elgg(), $args[0], $args[1], $args[2], $args[3]);
57 634
						break;
58
59 618
					case 'event' :
60 618
						$object = new HrsEvent(elgg(), $args[0], $args[1], $args[2]);
61 618
						break;
62
63
					case 'middleware' :
64
					case 'controller' :
65
					case 'action' :
66
						$object = new Request(elgg(), $args[0]);
67
						break;
68
				}
69
			}
70
71 1045
			$result = call_user_func($callable, $object);
72
		} else {
73
			// legacy arguments
74 248
			if ($this->getParamTypeForCallable($callable) !== null) {
75 59
				$described_callback = $this->describeCallable($callable);
76 59
				$msg = "Using legacy style hook and event callback arguments is deprecated. Used by: {$described_callback} for [{$args[0]}, {$args[1]}].";
77
				
78 59
				$msg_hash = md5($msg);
79 59
				if (!in_array($msg_hash, $this->deprecated_args_msgs)) {
80 57
					elgg_deprecated_notice($msg, "3.1");
81 57
					$this->deprecated_args_msgs[] = $msg_hash;
82
				}
83
			}
84
			
85 248
			$result = call_user_func_array($callable, $args);
86
		}
87
88 1089
		return [true, $result, $object];
89
	}
90
91
	/**
92
	 * Test is callback is callable
93
	 * Unlike is_callable(), this function also tests invokable classes
94
	 *
95
	 * @see is_callable()
96
	 *
97
	 * @param mixed $callback Callable
98
	 * @return bool
99
	 */
100 1
	public function isCallable($callback) {
101 1
		$callback = $this->resolveCallable($callback);
102 1
		return $callback && is_callable($callback);
103
	}
104
105
	/**
106
	 * Get the reflection interface for a callable
107
	 *
108
	 * @param callable $callable Callable
109
	 *
110
	 * @return \ReflectionFunctionAbstract
111
	 */
112 1105
	public function getReflector($callable) {
113 1105
		if (is_string($callable)) {
114 780
			if (false !== strpos($callable, '::')) {
115 11
				$callable = explode('::', $callable);
116
			} else {
117
				// function
118 775
				return new \ReflectionFunction($callable);
119
			}
120
		}
121 680
		if (is_array($callable)) {
122 380
			return new \ReflectionMethod($callable[0], $callable[1]);
123
		}
124 465
		if ($callable instanceof \Closure) {
125 287
			return new \ReflectionFunction($callable);
126
		}
127 252
		if (is_object($callable)) {
128 252
			return new \ReflectionMethod($callable, '__invoke');
129
		}
130
131
		throw new \InvalidArgumentException('invalid $callable');
132
	}
133
134
	/**
135
	 * 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 1104
	private function resolveCallable($callable) {
0 ignored issues
show
Private method name "HandlersService::resolveCallable" must be prefixed with an underscore
Loading history...
142 1104
		if (is_callable($callable)) {
143 986
			return $callable;
144
		}
145
146 253
		if (is_string($callable)
147 253
			&& preg_match(DiContainer::CLASS_NAME_PATTERN_53, $callable)
148 253
			&& class_exists($callable)) {
149
			// @todo Eventually a more advanced DIC could auto-inject dependencies
150 250
			$callable = new $callable;
151
		}
152
153 253
		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 1101
	private function acceptsObject($callable) {
0 ignored issues
show
Private method name "HandlersService::acceptsObject" must be prefixed with an underscore
Loading history...
164
		// note: caching string callables didn't help any
165 1101
		$type = (string) $this->getParamTypeForCallable($callable);
166 1101
		if (0 === strpos($type, 'Elgg\\')) {
167
			// probably right. We can just assume and let PHP handle it
168 1045
			return true;
169
		}
170
171 248
		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
	 * @return null|string Empty string = no type, null = no parameter
181
	 */
182 1105
	public function getParamTypeForCallable($callable, $index = 0) {
183 1105
		$params = $this->getReflector($callable)->getParameters();
184 1105
		if (!isset($params[$index])) {
185 196
			return null;
186
		}
187
188 1078
		return $this->getType($params[$index]);
189
	}
190
191
	/**
192
	 * Get the type of a parameter
193
	 *
194
	 * @param \ReflectionParameter $param Parameter
195
	 *
196
	 * @return string
197
	 */
198 1078
	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
202 1078
		if (method_exists($param, 'getType')
203 1078
				&& ($type = $param->getType())
204 1078
				&& $type->isBuiltin()) {
205 4
			return (string) $type;
206
		}
207
208
		// can be dropped when dropping PHP7 support:
209 1078
		if ($param->isArray()) {
210
			return 'array';
211
		}
212
213
		// can be dropped when dropping PHP7 support:
214 1078
		if ($param->isCallable()) {
215
			return 'callable';
216
		}
217
218
		// ReflectionParameter::__toString() doesn't require loading class
219 1078
		if (preg_match('~\[\s\<\w+?>\s([\S]+)~s', (string) $param, $m)) {
220 1078
			if ($m[1][0] !== '$') {
221 1049
				return $m[1];
222
			}
223
		}
224
225 63
		return '';
226
	}
227
228
	/**
229
	 * Get a string description of a callback
230
	 *
231
	 * E.g. "function_name", "Static::method", "(ClassName)->method", "(Closure path/to/file.php:23)"
232
	 *
233
	 * @param mixed  $callable  Callable
234
	 * @param string $file_root If provided, it will be removed from the beginning of file names
235
	 * @return string
236
	 */
237 67
	public function describeCallable($callable, $file_root = '') {
238 67
		if (is_string($callable)) {
239 6
			return $callable;
240
		}
241 65
		if (is_array($callable) && array_keys($callable) === [0, 1] && is_string($callable[1])) {
242 17
			if (is_string($callable[0])) {
243 2
				return "{$callable[0]}::{$callable[1]}";
244
			}
245 15
			return "(" . get_class($callable[0]) . ")->{$callable[1]}";
246
		}
247 54
		if ($callable instanceof \Closure) {
248 54
			$ref = new \ReflectionFunction($callable);
249 54
			$file = $ref->getFileName();
250 54
			$line = $ref->getStartLine();
251
252 54
			if ($file_root && 0 === strpos($file, $file_root)) {
253 4
				$file = substr($file, strlen($file_root));
254
			}
255
256 54
			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