Container::resolveParameterType()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Darya\Service;
4
5
use Closure;
6
use Darya\Service\Exceptions\ContainerException;
7
use Darya\Service\Exceptions\NotFoundException;
8
use InvalidArgumentException;
9
use ReflectionClass;
10
use ReflectionException;
11
use ReflectionMethod;
12
use ReflectionFunction;
13
use ReflectionParameter;
14
use Darya\Service\Contracts\ContainerAware;
15
use Darya\Service\Contracts\Container as ContainerInterface;
16
17
/**
18
 * Darya's service container.
19
 *
20
 * Service containers can be used to associate interfaces with implementations.
21
 * They ease interchanging the components and dependencies of an application.
22
 *
23
 * TODO: ArrayAccess
24
 * TODO: factory() method
25
 *
26
 * @author Chris Andrew <[email protected]>
27
 */
28
class Container implements ContainerInterface
29
{
30
	/**
31
	 * Set of abstract service names as keys and implementations as values.
32
	 *
33
	 * @var array
34
	 */
35
	protected $services = [];
36
37
	/**
38
	 * Set of aliases as keys and interfaces as values.
39
	 *
40
	 * @var array
41
	 */
42
	protected $aliases = [];
43
44
	/**
45
	 * A delegate container to resolve services from.
46
	 *
47
	 * @var ContainerInterface
48
	 */
49
	protected $delegate;
50
51
	/**
52
	 * Instantiate a service container.
53
	 *
54
	 * Registers the service container with itself, as well as registering any
55
	 * given services and aliases.
56
	 *
57
	 * @param array $services [optional] Initial set of services and/or aliases
58
	 */
59
	public function __construct(array $services = [])
60
	{
61
		$this->register([
62
			'Darya\Service\Contracts\Container' => $this,
63
			'Darya\Service\Container'           => $this
64
		]);
65
66
		$this->register($services);
67
	}
68
69
	/**
70
	 * Dynamically resolve a service.
71
	 *
72
	 * @param string $abstract The abstract service name
73
	 * @return mixed The resolved service
74
	 * @throws ContainerException
75
	 */
76
	public function __get($abstract)
77
	{
78
		return $this->get($abstract);
79
	}
80
81
	/**
82
	 * Dynamically register a service.
83
	 *
84
	 * @param string $abstract The abstract service name
85
	 * @param mixed  $service  The concrete service
86
	 */
87
	public function __set($abstract, $service)
88
	{
89
		$this->register([$abstract => $service]);
90
	}
91
92
	public function has($abstract)
93
	{
94
		return isset($this->aliases[$abstract]) || isset($this->services[$abstract]);
95
	}
96
97
	public function raw($abstract)
98
	{
99
		if (isset($this->aliases[$abstract])) {
100
			$abstract = $this->aliases[$abstract];
101
102
			return $this->raw($abstract);
103
		}
104
105
		if (isset($this->services[$abstract])) {
106
			return $this->services[$abstract];
107
		}
108
109
		if (isset($this->delegate)) {
110
			return $this->delegate->raw($abstract);
111
		}
112
113
		throw new NotFoundException("Service '$abstract' not found");
114
	}
115
116
	public function get($abstract, array $arguments = [])
117
	{
118
		$concrete = $this->raw($abstract);
119
120
		try {
121
			if ($concrete instanceof Closure || is_callable($concrete)) {
122
				return $this->call($concrete, $arguments ?: [$this]);
123
			}
124
125
			if (is_string($concrete)) {
126
				if ($abstract !== $concrete && $this->has($concrete)) {
127
					return $this->get($concrete, $arguments);
128
				}
129
130
				if (class_exists($concrete)) {
131
					return $this->create($concrete, $arguments);
132
				}
133
			}
134
		} catch (ContainerException $exception) {
135
			throw new ContainerException(
136
				"Error resolving service '$abstract'",
137
				$exception->getCode(),
138
				$exception
139
			);
140
		}
141
142
		return $concrete;
143
	}
144
145
	public function set($abstract, $concrete)
146
	{
147
		$this->services[$abstract] = is_callable($concrete) ? $this->share($concrete) : $concrete;
148
	}
149
150
	public function alias($alias, $abstract)
151
	{
152
		$this->aliases[$alias] = $abstract;
153
	}
154
155
	public function register(array $services)
156
	{
157
		foreach ($services as $key => $value) {
158
			if (is_string($value) && isset($this->services[$value])) {
159
				$this->alias($key, $value);
160
			} else {
161
				$this->set($key, $value);
162
			}
163
		}
164
	}
165
166
	public function share($callable)
167
	{
168
		if (!is_callable($callable)) {
169
			throw new InvalidArgumentException('Service is not callable');
170
		}
171
172
		$container = $this;
173
174
		return function () use ($callable, $container) {
175
			static $instance;
176
177
			if ($instance === null) {
178
				$instance = $container->call($callable, [$container]);
179
			}
180
181
			return $instance;
182
		};
183
	}
184
185
	public function call($callable, array $arguments = [])
186
	{
187
		if (!is_callable($callable)) {
188
			throw new ContainerException("Callable given is not callable");
189
		}
190
191
		$method = is_array($callable) && count($callable) > 1 && method_exists($callable[0], $callable[1]);
192
193
		try {
194
			if ($method) {
195
				$reflection = new ReflectionMethod($callable[0], $callable[1]);
196
			} else {
197
				$reflection = new ReflectionFunction($callable);
198
			}
199
200
			$parameters = $reflection->getParameters();
201
			$arguments  = $this->resolveParameterArguments($parameters, $arguments);
202
		} catch (ReflectionException $exception) {
203
			throw new ContainerException(
204
				"Error calling callable",
205
				$exception->getCode(),
206
				$exception
207
			);
208
		}
209
210
		return $method ? $reflection->invokeArgs($callable[0], $arguments) : $reflection->invokeArgs($arguments);
0 ignored issues
show
Unused Code introduced by
The call to ReflectionFunction::invokeArgs() has too many arguments starting with $arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
		return $method ? $reflection->/** @scrutinizer ignore-call */ invokeArgs($callable[0], $arguments) : $reflection->invokeArgs($arguments);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
The call to ReflectionMethod::invokeArgs() has too few arguments starting with args. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
		return $method ? $reflection->invokeArgs($callable[0], $arguments) : $reflection->/** @scrutinizer ignore-call */ invokeArgs($arguments);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$arguments of type array is incompatible with the type null|object expected by parameter $object of ReflectionMethod::invokeArgs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

210
		return $method ? $reflection->invokeArgs($callable[0], $arguments) : $reflection->invokeArgs(/** @scrutinizer ignore-type */ $arguments);
Loading history...
211
	}
212
213
	public function create($class, array $arguments = [])
214
	{
215
		try {
216
			$reflection  = new ReflectionClass($class);
217
			$constructor = $reflection->getConstructor();
218
219
			if (!$constructor) {
220
				return $reflection->newInstance();
221
			}
222
223
			$parameters = $constructor->getParameters();
224
			$arguments  = $this->resolveParameterArguments($parameters, $arguments);
225
		} catch (ReflectionException $exception) {
226
			throw new ContainerException(
227
				"Error creating instance of '$class'",
228
				$exception->getCode(),
229
				$exception
230
			);
231
		}
232
233
		$instance = $reflection->newInstanceArgs($arguments);
234
235
		if ($instance instanceof ContainerAware) {
236
			$instance->setServiceContainer($this);
237
		}
238
239
		return $instance;
240
	}
241
242
	/**
243
	 * Delegate a container to resolve services from when this container is
244
	 * unable to.
245
	 *
246
	 * @param ContainerInterface $container The container to delegate
247
	 */
248
	public function delegate(ContainerInterface $container)
249
	{
250
		$this->delegate = $container;
251
	}
252
253
	/**
254
	 * Merge resolved parameters arguments with the given arguments.
255
	 *
256
	 * TODO: Make this smarter.
257
	 *
258
	 * @param array $resolved  The resolved arguments
259
	 * @param array $arguments The given arguments
260
	 * @return array The merged arguments
261
	 */
262
	protected function mergeResolvedParameterArguments(array $resolved, array $arguments = [])
263
	{
264
		if (empty(array_filter(array_keys($arguments), 'is_numeric'))) {
265
			// We can perform a simple array merge if there are no numeric keys
266
			return array_merge($resolved, $arguments);
267
		}
268
269
		// Otherwise, we use the given arguments, falling back to resolved arguments
270
		// TODO: Some alternate merge involving numeric indexes, maybe?
271
		return $arguments ?: $resolved;
272
	}
273
274
	/**
275
	 * Resolve arguments for a set of reflection parameters.
276
	 *
277
	 * @param ReflectionParameter[] $parameters The parameters to resolve arguments for
278
	 * @param array                 $arguments  [optional] The given arguments
279
	 * @return array The resolved arguments keyed by parameter name
280
	 * @throws ContainerException
281
	 */
282
	protected function resolveParameterArguments(array $parameters, array $arguments = [])
283
	{
284
		$resolved = [];
285
286
		foreach ($parameters as $index => $parameter) {
287
			// TODO: Consider nullable parameters (array_keys_exists() vs isset())
288
			if (isset($arguments[$index])) {
289
				$resolved[$parameter->name] = $arguments[$index];
290
				continue;
291
			}
292
293
			if (isset($arguments[$parameter->name])) {
294
				$resolved[$parameter->name] = $arguments[$parameter->name];
295
				continue;
296
			}
297
298
			$argument                   = $this->resolveParameterArgument($parameter);
299
			$resolved[$parameter->name] = $argument;
300
		}
301
302
		$resolved = $this->mergeResolvedParameterArguments($resolved, $arguments);
303
304
		return $resolved;
305
	}
306
307
	/**
308
	 * Resolve an argument for a reflection parameter.
309
	 *
310
	 * @param ReflectionParameter|null $parameter The parameter to resolve an argument for
311
	 * @return mixed The resolved argument for the parameter
312
	 * @throws ContainerException
313
	 */
314
	protected function resolveParameterArgument(ReflectionParameter $parameter)
315
	{
316
		$type = $this->resolveParameterType($parameter);
317
318
		if ($type !== null) {
319
			if ($this->has($type)) {
320
				return $this->get($type);
321
			}
322
323
			if (class_exists($type)) {
324
				return $this->create($type);
325
			}
326
		}
327
328
		if ($parameter->isDefaultValueAvailable()) {
329
			try {
330
				return $parameter->getDefaultValue();
331
			} catch (ReflectionException $exception) {
332
				// We want to continue to the exception below
333
				// when a default value cannot be resolved
334
			}
335
		}
336
337
		// TODO: Include reflection exception as previous exception?
338
		throw new ContainerException("Unresolvable parameter '\${$parameter->name}'");
339
	}
340
341
	/**
342
	 * Resolve the class type hint of a reflection parameter.
343
	 *
344
	 * @param ReflectionParameter $parameter The parameter to resolve class type hint for
345
	 * @return string|null The class type hint of the reflection parameter
346
	 */
347
	protected function resolveParameterType(ReflectionParameter $parameter)
348
	{
349
		$class = $parameter->getClass();
350
351
		return is_object($class) ? $class->name : null;
352
	}
353
}
354