Completed
Push — master ( 337188...6b7fea )
by Chris
04:18
created

Container::delegate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Darya\Service;
3
4
use Closure;
5
use ReflectionClass;
6
use ReflectionMethod;
7
use ReflectionFunction;
8
use ReflectionParameter;
9
use Darya\Service\Contracts\ContainerAware;
10
use Darya\Service\Contracts\Container as ContainerInterface;
11
12
/**
13
 * Darya's service container.
14
 * 
15
 * Service containers can be used to associate interfaces with implementations.
16
 * They ease interchanging the components and dependencies of an application.
17
 * 
18
 * TODO: ArrayAccess
19
 * TODO: Delegate container
20
 * TODO: factory() method
21
 * TODO: ContainerInterop: get() resolves dependencies, new raw() won't
22
 * 
23
 * @author Chris Andrew <[email protected]>
24
 */
25
class Container implements ContainerInterface
26
{
27
	/**
28
	 * Set of abstracts as keys and implementations as values.
29
	 * 
30
	 * @var array
31
	 */
32
	protected $services = array();
33
	
34
	/**
35
	 * Set of aliases as keys and interfaces as values.
36
	 * 
37
	 * @var array
38
	 */
39
	protected $aliases = array();
40
	
41
	/**
42
	 * A delegate container to resolve services from.
43
	 *
44
	 * @var ContainerInterface
45
	 */
46
	protected $delegate;
47
	
48
	/**
49
	 * Instantiate a service container.
50
	 * 
51
	 * Registers the service container with itself, as well as registering any
52
	 * given services and aliases.
53
	 * 
54
	 * @param array $services [optional] Initial set of services and/or aliases
55
	 */
56
	public function __construct(array $services = array())
57
	{
58
		$this->register(array(
59
			'Darya\Service\Contracts\Container' => $this,
60
			'Darya\Service\Container'           => $this
61
		));
62
		
63
		$this->register($services);
64
	}
65
	
66
	/**
67
	 * Dynamically resolve a service.
68
	 * 
69
	 * @param string $abstract
70
	 * @return mixed
71
	 */
72
	public function __get($abstract)
73
	{
74
		return $this->resolve($abstract);
75
	}
76
	
77
	/**
78
	 * Dynamically register a service.
79
	 * 
80
	 * @param string $abstract
81
	 * @param mixed  $service
82
	 */
83
	public function __set($abstract, $service)
84
	{
85
		$this->register(array($abstract => $service));
86
	}
87
	
88
	/**
89
	 * Determine whether the container has a service registered for the given
90
	 * abstract or alias.
91
	 * 
92
	 * @param string $abstract
93
	 * @return bool
94
	 */
95
	public function has($abstract)
96
	{
97
		return isset($this->aliases[$abstract]) || isset($this->services[$abstract]);
98
	}
99
	
100
	/**
101
	 * Get the service associated with the given abstract or alias.
102
	 * 
103
	 * This method recursively resolves aliases but does not resolve service
104
	 * dependencies.
105
	 * 
106
	 * Returns null if nothing is found.
107
	 * 
108
	 * @param string $abstract
109
	 * @return mixed
110
	 */
111
	public function get($abstract)
112
	{
113
		if (isset($this->aliases[$abstract])) {
114
			$abstract = $this->aliases[$abstract];
115
			
116
			return $this->get($abstract);
117
		}
118
		
119
		if (isset($this->services[$abstract])) {
120
			return $this->services[$abstract];
121
		}
122
		
123
		if (isset($this->delegate)) {
124
			return $this->delegate->get($abstract);
125
		}
126
		
127
		return null;
128
	}
129
	
130
	/**
131
	 * Register a service and its associated implementation.
132
	 * 
133
	 * @param string $abstract
134
	 * @param mixed  $concrete
135
	 */
136
	public function set($abstract, $concrete)
137
	{
138
		$this->services[$abstract] = is_callable($concrete) ? $this->share($concrete) : $concrete;
139
	}
140
	
141
	/**
142
	 * Retrieve all registered services.
143
	 * 
144
	 * @return array
145
	 */
146
	public function all()
147
	{
148
		return $this->services;
149
	}
150
	
151
	/**
152
	 * Register an alias for the given abstract.
153
	 * 
154
	 * @param string $alias
155
	 * @param string $abstract
156
	 */
157
	public function alias($alias, $abstract)
158
	{
159
		$this->aliases[$alias] = $abstract;
160
	}
161
	
162
	/**
163
	 * Register services and aliases.
164
	 * 
165
	 * This method registers aliases if their abstract is already
166
	 * registered with the container.
167
	 * 
168
	 * @param array $services abstract => concrete and/or alias => abstract
169
	 */
170
	public function register(array $services = array())
171
	{
172
		foreach ($services as $key => $value) {
173
			if (is_string($value) && isset($this->services[$value])) {
174
				$this->alias($key, $value);
175
			} else {
176
				$this->set($key, $value);
177
			}
178
		}
179
	}
180
181
	/**
182
	 * Resolve a service and its dependencies.
183
	 * 
184
	 * This method recursively resolves services and aliases.
185
	 * 
186
	 * @param string $abstract  Abstract or alias
187
	 * @param array  $arguments [optional]
188
	 * @return mixed
189
	 */
190
	public function resolve($abstract, array $arguments = array())
191
	{
192
		$concrete = $this->get($abstract);
193
		
194
		if ($concrete instanceof Closure || is_callable($concrete)) {
195
			return $this->call($concrete, $arguments ?: array($this));
196
		}
197
		
198
		if (is_string($concrete)) {
199
			if ($abstract !== $concrete && $this->has($concrete)) {
200
				return $this->resolve($concrete, $arguments);
201
			}
202
			
203
			if (class_exists($concrete)) {
204
				return $this->create($concrete, $arguments);
205
			}
206
		}
207
		
208
		return $concrete;
209
	}
210
	
211
	/**
212
	 * Wraps a callable in a closure that returns the same instance on every
213
	 * call using a static variable.
214
	 * 
215
	 * @param callable $callable
216
	 * @return Closure
217
	 * @throws ContainerException
218
	 */
219
	public function share($callable)
220
	{
221
		if (!is_callable($callable)) {
222
			throw new ContainerException('Service is not callable in Container::share()');
223
		}
224
		
225
		$container = $this;
226
		
227
		return function () use ($callable, $container) {
228
			static $instance;
229
			
230
			if ($instance === null) {
231
				$instance = $container->call($callable, array($container));
232
			}
233
			
234
			return $instance;
235
		};
236
	}
237
	
238
	/**
239
	 * Call a callable and attempt to resolve its parameters using services
240
	 * registered with the container.
241
	 * 
242
	 * @param callable $callable
243
	 * @param array    $arguments [optional]
244
	 * @return mixed
245
	 */
246
	public function call($callable, array $arguments = array())
247
	{
248
		if (!is_callable($callable)) {
249
			return null;
250
		}
251
		
252
		$method = is_array($callable) && count($callable) > 1 && method_exists($callable[0], $callable[1]);
253
		
254
		if ($method) {
255
			$reflection = new ReflectionMethod($callable[0], $callable[1]);
256
		} else {
257
			$reflection = new ReflectionFunction($callable);
258
		}
259
		
260
		$parameters = $reflection->getParameters();
261
		$arguments = $this->resolveParameters($parameters, $arguments);
262
		
263
		return $method ? $reflection->invokeArgs($callable[0], $arguments) : $reflection->invokeArgs($arguments);
264
	}
265
	
266
	/**
267
	 * Instantiate the given class and attempt to resolve its constructor's
268
	 * parameters using services registered with the container.
269
	 * 
270
	 * @param string $class
271
	 * @param array  $arguments [optional]
272
	 * @return object
273
	 */
274
	public function create($class, array $arguments = array())
275
	{
276
		$reflection = new ReflectionClass($class);
277
		$constructor = $reflection->getConstructor();
278
		
279
		if (!$constructor) {
280
			return $reflection->newInstance();
281
		}
282
		
283
		$parameters = $constructor->getParameters();
284
		$arguments = $this->resolveParameters($parameters, $arguments);
285
		
286
		$instance = $reflection->newInstanceArgs($arguments);
287
		
288
		if ($instance instanceof ContainerAware) {
289
			$instance->setServiceContainer($this);
290
		}
291
		
292
		return $instance;
293
	}
294
	
295
	/**
296
	 * Delegate a container to resolve services from when this container is
297
	 * unable to.
298
	 *
299
	 * @param ContainerInterface $container
300
	 */
301
	public function delegate(ContainerInterface $container)
302
	{
303
		$this->delegate = $container;
304
	}
305
	
306
	/**
307
	 * Merge resolved parameters with the given arguments.
308
	 * 
309
	 * TODO: Make this smarter.
310
	 * 
311
	 * @param array $resolved
312
	 * @param array $arguments
313
	 * @return array
314
	 */
315
	protected function mergeResolvedParameters(array $resolved, array $arguments = array())
316
	{
317
		if (!array_filter(array_keys($arguments), 'is_numeric')) {
318
			return array_merge($resolved, $arguments);
319
		} else {
320
			// Some alternate merge involving numeric indexes, maybe?
321
			return $arguments ?: $resolved;
322
		}
323
	}
324
	
325
	/**
326
	 * Resolve a set of reflection parameters.
327
	 * 
328
	 * @param ReflectionParameter[] $parameters
329
	 * @param array                 $arguments [optional]
330
	 * @return array
331
	 */
332
	protected function resolveParameters($parameters, array $arguments = array())
333
	{
334
		$resolved = array();
335
		
336
		foreach ($parameters as $parameter) {
337
			$argument = $this->resolveParameter($parameter);
338
			$resolved[$parameter->name] = $argument;
339
		}
340
		
341
		$resolved = $this->mergeResolvedParameters($resolved, $arguments);
342
		
343
		return $resolved;
344
	}
345
	
346
	/**
347
	 * Attempt to resolve a reflection parameter's argument.
348
	 * 
349
	 * @param ReflectionParameter|null $parameter
350
	 * @return mixed
351
	 */
352
	protected function resolveParameter(ReflectionParameter $parameter)
353
	{
354
		$type = $this->resolveParameterType($parameter);
355
		
356
		if ($type !== null) {
357
			if ($this->has($type)) {
358
				return $this->resolve($type);
359
			}
360
			
361
			if (class_exists($type)) {
362
				return $this->create($type);
363
			}
364
		}
365
		
366
		if ($parameter->isDefaultValueAvailable()) {
367
			return $parameter->getDefaultValue();
368
		}
369
		
370
		return null;
371
	}
372
	
373
	/**
374
	 * Resolve the given reflection parameters type hint.
375
	 * 
376
	 * @param ReflectionParameter $parameter
377
	 * @return string|null
378
	 */
379
	protected function resolveParameterType(ReflectionParameter $parameter)
380
	{
381
		$class = $parameter->getClass();
382
		
383
		return is_object($class) ? $class->name : null;
384
	}
385
}
386