1 <?php
2 /**
3 * Part of the Joomla Framework DI Package
4 *
5 * @copyright Copyright (C) 2013 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\DI;
10
11 use Joomla\DI\Exception\DependencyResolutionException;
12
13 /**
14 * The Container class.
15 *
16 * @since 1.0
17 */
18 class Container
19 {
20 /**
21 * Holds the key aliases.
22 *
23 * @var array $aliases
24 * @since 1.0
25 */
26 protected $aliases = array();
27
28 /**
29 * Holds the shared instances.
30 *
31 * @var array $instances
32 * @since 1.0
33 */
34 protected $instances = array();
35
36 /**
37 * Holds the keys, their callbacks, and whether or not
38 * the item is meant to be a shared resource.
39 *
40 * @var array $dataStore
41 * @since 1.0
42 */
43 protected $dataStore = array();
44
45 /**
46 * Parent for hierarchical containers.
47 *
48 * @var Container
49 * @since 1.0
50 */
51 protected $parent;
52
53 /**
54 * Constructor for the DI Container
55 *
56 * @param Container $parent Parent for hierarchical containers.
57 *
58 * @since 1.0
59 */
60 public function __construct(Container $parent = null)
61 {
62 $this->parent = $parent;
63 }
64
65 /**
66 * Create an alias for a given key for easy access.
67 *
68 * @param string $alias The alias name
69 * @param string $key The key to alias
70 *
71 * @return Container This object for chaining.
72 *
73 * @since 1.0
74 */
75 public function alias($alias, $key)
76 {
77 $this->aliases[$alias] = $key;
78
79 return $this;
80 }
81
82 /**
83 * Search the aliases property for a matching alias key.
84 *
85 * @param string $key The key to search for.
86 *
87 * @return string
88 *
89 * @since 1.0
90 */
91 protected function resolveAlias($key)
92 {
93 if (isset($this->aliases[$key]))
94 {
95 return $this->aliases[$key];
96 }
97
98 if ($this->parent instanceof Container)
99 {
100 return $this->parent->resolveAlias($key);
101 }
102
103 return $key;
104 }
105
106 /**
107 * Build an object of class $key;
108 *
109 * @param string $key The class name to build.
110 * @param boolean $shared True to create a shared resource.
111 *
112 * @return mixed Instance of class specified by $key with all dependencies injected.
113 * Returns an object if the class exists and false otherwise
114 *
115 * @since 1.0
116 */
117 public function buildObject($key, $shared = false)
118 {
119 try
120 {
121 $reflection = new \ReflectionClass($key);
122 }
123 catch (\ReflectionException $e)
124 {
125 return false;
126 }
127
128 $constructor = $reflection->getConstructor();
129
130 // If there are no parameters, just return a new object.
131 if (is_null($constructor))
132 {
133 $callback = function () use ($key) {
134 return new $key;
135 };
136 }
137 else
138 {
139 $newInstanceArgs = $this->getMethodArgs($constructor);
140
141 // Create a callable for the dataStore
142 $callback = function () use ($reflection, $newInstanceArgs) {
143 return $reflection->newInstanceArgs($newInstanceArgs);
144 };
145 }
146
147 return $this->set($key, $callback, $shared)->get($key);
148 }
149
150 /**
151 * Convenience method for building a shared object.
152 *
153 * @param string $key The class name to build.
154 *
155 * @return object Instance of class specified by $key with all dependencies injected.
156 *
157 * @since 1.0
158 */
159 public function buildSharedObject($key)
160 {
161 return $this->buildObject($key, true);
162 }
163
164 /**
165 * Create a child Container with a new property scope that
166 * that has the ability to access the parent scope when resolving.
167 *
168 * @return Container This object for chaining.
169 *
170 * @since 1.0
171 */
172 public function createChild()
173 {
174 return new static($this);
175 }
176
177 /**
178 * Extend a defined service Closure by wrapping the existing one with a new Closure. This
179 * works very similar to a decorator pattern. Note that this only works on service Closures
180 * that have been defined in the current Provider, not parent providers.
181 *
182 * @param string $key The unique identifier for the Closure or property.
183 * @param \Closure $callable A Closure to wrap the original service Closure.
184 *
185 * @return void
186 *
187 * @since 1.0
188 * @throws \InvalidArgumentException
189 */
190 public function extend($key, \Closure $callable)
191 {
192 $key = $this->resolveAlias($key);
193 $raw = $this->getRaw($key);
194
195 if (is_null($raw))
196 {
197 throw new \InvalidArgumentException(sprintf('The requested key %s does not exist to extend.', $key));
198 }
199
200 $closure = function ($c) use($callable, $raw) {
201 return $callable($raw['callback']($c), $c);
202 };
203
204 $this->set($key, $closure, $raw['shared']);
205 }
206
207 /**
208 * Build an array of constructor parameters.
209 *
210 * @param \ReflectionMethod $method Method for which to build the argument array.
211 *
212 * @return array Array of arguments to pass to the method.
213 *
214 * @since 1.0
215 * @throws DependencyResolutionException
216 */
217 protected function getMethodArgs(\ReflectionMethod $method)
218 {
219 $methodArgs = array();
220
221 foreach ($method->getParameters() as $param)
222 {
223 $dependency = $param->getClass();
224 $dependencyVarName = $param->getName();
225
226 // If we have a dependency, that means it has been type-hinted.
227 if (!is_null($dependency))
228 {
229 $dependencyClassName = $dependency->getName();
230
231 // If the dependency class name is registered with this container or a parent, use it.
232 if ($this->getRaw($dependencyClassName) !== null)
233 {
234 $depObject = $this->get($dependencyClassName);
235 }
236 else
237 {
238 $depObject = $this->buildObject($dependencyClassName);
239 }
240
241 if ($depObject instanceof $dependencyClassName)
242 {
243 $methodArgs[] = $depObject;
244 continue;
245 }
246 }
247
248 // Finally, if there is a default parameter, use it.
249 if ($param->isOptional())
250 {
251 $methodArgs[] = $param->getDefaultValue();
252 continue;
253 }
254
255 // Couldn't resolve dependency, and no default was provided.
256 throw new DependencyResolutionException(sprintf('Could not resolve dependency: %s', $dependencyVarName));
257 }
258
259 return $methodArgs;
260 }
261
262 /**
263 * Method to set the key and callback to the dataStore array.
264 *
265 * @param string $key Name of dataStore key to set.
266 * @param mixed $value Callable function to run or string to retrive when requesting the specified $key.
267 * @param boolean $shared True to create and store a shared instance.
268 * @param boolean $protected True to protect this item from being overwritten. Useful for services.
269 *
270 * @return Container This object for chaining.
271 *
272 * @throws \OutOfBoundsException Thrown if the provided key is already set and is protected.
273 *
274 * @since 1.0
275 */
276 public function set($key, $value, $shared = false, $protected = false)
277 {
278 if (isset($this->dataStore[$key]) && $this->dataStore[$key]['protected'] === true)
279 {
280 throw new \OutOfBoundsException(sprintf('Key %s is protected and can\'t be overwritten.', $key));
281 }
282
283 // If the provided $value is not a closure, make it one now for easy resolution.
284 if (!is_callable($value))
285 {
286 $value = function () use ($value) {
287 return $value;
288 };
289 }
290
291 $this->dataStore[$key] = array(
292 'callback' => $value,
293 'shared' => $shared,
294 'protected' => $protected
295 );
296
297 return $this;
298 }
299
300 /**
301 * Convenience method for creating protected keys.
302 *
303 * @param string $key Name of dataStore key to set.
304 * @param callable $callback Callable function to run when requesting the specified $key.
305 * @param bool $shared True to create and store a shared instance.
306 *
307 * @return Container This object for chaining.
308 *
309 * @since 1.0
310 */
311 public function protect($key, $callback, $shared = false)
312 {
313 return $this->set($key, $callback, $shared, true);
314 }
315
316 /**
317 * Convenience method for creating shared keys.
318 *
319 * @param string $key Name of dataStore key to set.
320 * @param callable $callback Callable function to run when requesting the specified $key.
321 * @param bool $protected True to create and store a shared instance.
322 *
323 * @return Container This object for chaining.
324 *
325 * @since 1.0
326 */
327 public function share($key, $callback, $protected = false)
328 {
329 return $this->set($key, $callback, true, $protected);
330 }
331
332 /**
333 * Method to retrieve the results of running the $callback for the specified $key;
334 *
335 * @param string $key Name of the dataStore key to get.
336 * @param boolean $forceNew True to force creation and return of a new instance.
337 *
338 * @return mixed Results of running the $callback for the specified $key.
339 *
340 * @since 1.0
341 * @throws \InvalidArgumentException
342 */
343 public function get($key, $forceNew = false)
344 {
345 $key = $this->resolveAlias($key);
346 $raw = $this->getRaw($key);
347
348 if (is_null($raw))
349 {
350 throw new \InvalidArgumentException(sprintf('Key %s has not been registered with the container.', $key));
351 }
352
353 if ($raw['shared'])
354 {
355 if (!isset($this->instances[$key]) || $forceNew)
356 {
357 $this->instances[$key] = $raw['callback']($this);
358 }
359
360 return $this->instances[$key];
361 }
362
363 return call_user_func($raw['callback'], $this);
364 }
365
366 /**
367 * Method to check if specified dataStore key exists.
368 *
369 * @param string $key Name of the dataStore key to check.
370 *
371 * @return boolean True for success
372 *
373 * @since 1.0
374 */
375 public function exists($key)
376 {
377 $key = $this->resolveAlias($key);
378
379 return (bool) $this->getRaw($key);
380 }
381
382 /**
383 * Get the raw data assigned to a key.
384 *
385 * @param string $key The key for which to get the stored item.
386 *
387 * @return mixed
388 *
389 * @since 1.0
390 */
391 protected function getRaw($key)
392 {
393 if (isset($this->dataStore[$key]))
394 {
395 return $this->dataStore[$key];
396 }
397
398 $aliasKey = $this->resolveAlias($key);
399
400 if ($aliasKey != $key && isset($this->dataStore[$aliasKey]))
401 {
402 return $this->dataStore[$aliasKey];
403 }
404
405 if ($this->parent instanceof Container)
406 {
407 return $this->parent->getRaw($key);
408 }
409
410 return null;
411 }
412
413 /**
414 * Method to force the container to return a new instance
415 * of the results of the callback for requested $key.
416 *
417 * @param string $key Name of the dataStore key to get.
418 *
419 * @return mixed Results of running the $callback for the specified $key.
420 *
421 * @since 1.0
422 */
423 public function getNewInstance($key)
424 {
425 return $this->get($key, true);
426 }
427
428 /**
429 * Register a service provider to the container.
430 *
431 * @param ServiceProviderInterface $provider The service provider to register.
432 *
433 * @return Container This object for chaining.
434 *
435 * @since 1.0
436 */
437 public function registerServiceProvider(ServiceProviderInterface $provider)
438 {
439 $provider->register($this);
440
441 return $this;
442 }
443 }
444