Issues (5)

src/Container.php (2 issues)

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2017 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Container;
9
10
use Psr\Container\ContainerInterface;
11
12
/**
13
 * An inversion of control container.
14
 */
15
class Container implements ContainerInterface {
16
    private $currentRule;
17
    private $currentRuleName;
18
    private $instances;
19
    private $rules;
20
    private $factories;
21
22
    /**
23
     * Construct a new instance of the {@link Container} class.
24
     */
25 100
    public function __construct() {
26 100
        $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => null]];
27 100
        $this->instances = [];
28 100
        $this->factories = [];
29
30 100
        $this->rule('*');
31 100
    }
32
33
    /**
34
     * Deep clone rules.
35
     */
36 1
    public function __clone() {
37 1
        $this->rules = $this->arrayClone($this->rules);
38 1
        $this->rule($this->currentRuleName);
39 1
    }
40
41
    /**
42
     * Clear all instances
43
     *
44
     */
45 1
    public function clearInstances() {
46 1
        $this->instances = [];
47 1
    }
48
49
    /**
50
     * Deep clone an array.
51
     *
52
     * @param array $array The array to clone.
53
     * @return array Returns the cloned array.
54
     * @see http://stackoverflow.com/a/17729234
55
     */
56 1
    private function arrayClone(array $array) {
57
        return array_map(function ($element) {
58 1
            return ((is_array($element))
59 1
                ? $this->arrayClone($element)
60 1
                : ((is_object($element))
61
                    ? clone $element
62 1
                    : $element
63
                )
64
            );
65 1
        }, $array);
66
    }
67
68
    /**
69
     * Normalize a container entry ID.
70
     *
71
     * @param string $id The ID to normalize.
72
     * @return string Returns a normalized ID as a string.
73
     */
74 101
    private function normalizeID($id) {
75 101
        return ltrim($id, '\\');
76
    }
77
78
    /**
79
     * Set the current rule to the default rule.
80
     *
81
     * @return $this
82
     */
83 1
    public function defaultRule() {
84 1
        return $this->rule('*');
85
    }
86
87
    /**
88
     * Set the current rule.
89
     *
90
     * @param string $id The ID of the rule.
91
     * @return $this
92
     */
93 100
    public function rule($id) {
94 100
        $id = $this->normalizeID($id);
95
96 100
        if (!isset($this->rules[$id])) {
97 47
            $this->rules[$id] = [];
98
        }
99 100
        $this->currentRuleName = $id;
100 100
        $this->currentRule = &$this->rules[$id];
101
102 100
        return $this;
103
    }
104
105
    /**
106
     * Get the class name of the current rule.
107
     *
108
     * @return string Returns a class name.
109
     */
110 2
    public function getClass() {
111 2
        return empty($this->currentRule['class']) ? '' : $this->currentRule['class'];
112
    }
113
114
    /**
115
     * Set the name of the class for the current rule.
116
     *
117
     * @param string $className A valid class name.
118
     * @return $this
119
     */
120 10
    public function setClass($className) {
121 10
        $this->currentRule['class'] = $className;
122 10
        return $this;
123
    }
124
125
    /**
126
     * Get the rule that the current rule references.
127
     *
128
     * @return string Returns a reference name or an empty string if there is no reference.
129
     */
130 3
    public function getAliasOf() {
131 3
        return empty($this->currentRule['aliasOf']) ? '' : $this->currentRule['aliasOf'];
132
    }
133
134
    /**
135
     * Set the rule that the current rule is an alias of.
136
     *
137
     * @param string $alias The name of an entry in the container to point to.
138
     * @return $this
139
     */
140 4
    public function setAliasOf($alias) {
141 4
        $alias = $this->normalizeID($alias);
142
143 4
        if ($alias === $this->currentRuleName) {
144 1
            trigger_error("You cannot set alias '$alias' to itself.", E_USER_NOTICE);
145
        } else {
146 3
            $this->currentRule['aliasOf'] = $alias;
147
        }
148 4
        return $this;
149
    }
150
151
    /**
152
     * Add an alias of the current rule.
153
     *
154
     * Setting an alias to the current rule means that getting an item with the alias' name will be like getting the item
155
     * with the current rule. If the current rule is shared then the same shared instance will be returned. You can add
156
     * multiple aliases by passing additional arguments to this method.
157
     *
158
     * If {@link Container::addAlias()} is called with an alias that is the same as the current rule then an **E_USER_NOTICE**
159
     * level error is raised and the alias is not added.
160
     *
161
     * @param string ...$alias The alias to set.
162
     * @return $this
163
     * @since 1.4 Added the ability to pass multiple aliases.
164
     */
165 8
    public function addAlias(...$alias) {
166 8
        foreach ($alias as $name) {
167 8
            $name = $this->normalizeID($name);
168
169 8
            if ($name === $this->currentRuleName) {
170 1
                trigger_error("Tried to set alias '$name' to self.", E_USER_NOTICE);
171
            } else {
172 7
                $this->rules[$name]['aliasOf'] = $this->currentRuleName;
173
            }
174
        }
175 8
        return $this;
176
    }
177
178
    /**
179
     * Remove an alias of the current rule.
180
     *
181
     * If {@link Container::removeAlias()} is called with an alias that references a different rule then an **E_USER_NOTICE**
182
     * level error is raised, but the alias is still removed.
183
     *
184
     * @param string $alias The alias to remove.
185
     * @return $this
186
     */
187 2
    public function removeAlias($alias) {
188 2
        $alias = $this->normalizeID($alias);
189
190 2
        if (!empty($this->rules[$alias]['aliasOf']) && $this->rules[$alias]['aliasOf'] !== $this->currentRuleName) {
191 1
            trigger_error("Alias '$alias' does not point to the current rule.", E_USER_NOTICE);
192
        }
193
194 2
        unset($this->rules[$alias]['aliasOf']);
195 2
        return $this;
196
    }
197
198
    /**
199
     * Get all of the aliases of the current rule.
200
     *
201
     * This method is intended to aid in debugging and should not be used in production as it walks the entire rule array.
202
     *
203
     * @return array Returns an array of strings representing aliases.
204
     */
205 6
    public function getAliases() {
206 6
        $result = [];
207
208 6
        foreach ($this->rules as $name => $rule) {
209 6
            if (!empty($rule['aliasOf']) && $rule['aliasOf'] === $this->currentRuleName) {
210 4
                $result[] = $name;
211
            }
212
        }
213
214 6
        return $result;
215
    }
216
217
    /**
218
     * Get the factory callback for the current rule.
219
     *
220
     * @return callable|null Returns the rule's factory or **null** if it has none.
221
     */
222 2
    public function getFactory() {
223 2
        return isset($this->currentRule['factory']) ? $this->currentRule['factory'] : null;
224
    }
225
226
    /**
227
     * Set the factory that will be used to create the instance for the current rule.
228
     *
229
     * @param callable|null $factory This callback will be called to create the instance for the rule.
230
     * @return $this
231
     */
232 10
    public function setFactory(callable $factory = null) {
233 10
        $this->currentRule['factory'] = $factory;
234 10
        return $this;
235
    }
236
237
    /**
238
     * Whether or not the current rule is shared.
239
     *
240
     * @return bool Returns **true** if the rule is shared or **false** otherwise.
241
     */
242 2
    public function isShared() {
243 2
        return !empty($this->currentRule['shared']);
244
    }
245
246
    /**
247
     * Set whether or not the current rule is shared.
248
     *
249
     * @param bool $shared Whether or not the current rule is shared.
250
     * @return $this
251
     */
252 47
    public function setShared($shared) {
253 47
        $this->currentRule['shared'] = $shared;
254 47
        return $this;
255
    }
256
257
    /**
258
     * Whether or not the current rule will inherit to subclasses.
259
     *
260
     * @return bool Returns **true** if the current rule inherits or **false** otherwise.
261
     */
262 2
    public function getInherit() {
263 2
        return !empty($this->currentRule['inherit']);
264
    }
265
266
    /**
267
     * Set whether or not the current rule extends to subclasses.
268
     *
269
     * @param bool $inherit Pass **true** to have subclasses inherit this rule or **false** otherwise.
270
     * @return $this
271
     */
272 3
    public function setInherit($inherit) {
273 3
        $this->currentRule['inherit'] = $inherit;
274 3
        return $this;
275
    }
276
277
    /**
278
     * Get the constructor arguments for the current rule.
279
     *
280
     * @return array Returns the constructor arguments for the current rule.
281
     */
282 2
    public function getConstructorArgs() {
283 2
        return empty($this->currentRule['constructorArgs']) ? [] : $this->currentRule['constructorArgs'];
284
    }
285
286
    /**
287
     * Set the constructor arguments for the current rule.
288
     *
289
     * @param array $args An array of constructor arguments.
290
     * @return $this
291
     */
292 27
    public function setConstructorArgs(array $args) {
293 27
        $this->currentRule['constructorArgs'] = $args;
294 27
        return $this;
295
    }
296
297
    /**
298
     * Set a specific shared instance into the container.
299
     *
300
     * When you set an instance into the container then it will always be returned by subsequent retrievals, even if a
301
     * rule is configured that says that instances should not be shared.
302
     *
303
     * @param string $name The name of the container entry.
304
     * @param mixed $instance This instance.
305
     * @return $this
306
     */
307 9
    public function setInstance($name, $instance) {
308 9
        $this->instances[$this->normalizeID($name)] = $instance;
309 9
        return $this;
310
    }
311
312
    /**
313
     * Add a method call to a rule.
314
     *
315
     * @param string $method The name of the method to call.
316
     * @param array $args The arguments to pass to the method.
317
     * @return $this
318
     */
319 12
    public function addCall($method, array $args = []) {
320 12
        $this->currentRule['calls'][] = [$method, $args];
321
322 12
        return $this;
323
    }
324
325
    /**
326
     * Finds an entry of the container by its identifier and returns it.
327
     *
328
     * @param string $id Identifier of the entry to look for.
329
     * @param array $args Additional arguments to pass to the constructor.
330
     *
331
     * @throws NotFoundException No entry was found for this identifier.
332
     * @throws ContainerException Error while retrieving the entry.
333
     *
334
     * @return mixed Entry.
335
     */
336 81
    public function getArgs($id, array $args = []) {
337 81
        $id = $this->normalizeID($id);
338
339 81
        if (isset($this->instances[$id])) {
340
            // A shared instance just gets returned.
341 22
            return $this->instances[$id];
342
        }
343
344 77
        if (isset($this->factories[$id])) {
345
            // The factory for this object type is already there so call it to create the instance.
346 6
            return $this->factories[$id]($args);
347
        }
348
349 77
        if (!empty($this->rules[$id]['aliasOf'])) {
350
            // This rule references another rule.
351 3
            return $this->getArgs($this->rules[$id]['aliasOf'], $args);
352
        }
353
354
        // The factory or instance isn't registered so do that now.
355
        // This call also caches the instance or factory fo faster access next time.
356 77
        return $this->createInstance($id, $args);
357
    }
358
359
    /**
360
     * Make a rule based on an ID.
361
     *
362
     * @param string $nid A normalized ID.
363
     * @return array Returns an array representing a rule.
364
     */
365 77
    private function makeRule($nid) {
366 77
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
367
368 77
        if (class_exists($nid)) {
369 68
            for ($class = get_parent_class($nid); !empty($class); $class = get_parent_class($class)) {
370
                // Don't add the rule if it doesn't say to inherit.
371 6
                if (!isset($this->rules[$class]) || (isset($this->rules[$class]['inherit']) && !$this->rules[$class]['inherit'])) {
372 4
                    continue;
373
                }
374 2
                $rule += $this->rules[$class];
375
            }
376
377
            // Add the default rule.
378 68
            if (!empty($this->rules['*']['inherit'])) {
379 68
                $rule += $this->rules['*'];
380
            }
381
382
            // Add interface calls to the rule.
383 68
            $interfaces = class_implements($nid);
384 68
            foreach ($interfaces as $interface) {
385 44
                if (isset($this->rules[$interface])) {
386 10
                    $interfaceRule = $this->rules[$interface];
387
388 10
                    if (isset($interfaceRule['inherit']) && $interfaceRule['inherit'] === false) {
389 1
                        continue;
390
                    }
391
392 9
                    if (!isset($rule['shared']) && isset($interfaceRule['shared'])) {
393 3
                        $rule['shared'] = $interfaceRule['shared'];
394
                    }
395
396 9
                    if (!isset($rule['constructorArgs']) && isset($interfaceRule['constructorArgs'])) {
397 3
                        $rule['constructorArgs'] = $interfaceRule['constructorArgs'];
398
                    }
399
400 9
                    if (!empty($interfaceRule['calls'])) {
401 2
                        $rule['calls'] = array_merge(
402 2
                            isset($rule['calls']) ? $rule['calls'] : [],
403 2
                            $interfaceRule['calls']
404
                        );
405
                    }
406
                }
407
            }
408 13
        } elseif (!empty($this->rules['*']['inherit'])) {
409
            // Add the default rule.
410 13
            $rule += $this->rules['*'];
411
        }
412
413 77
        return $rule;
414
    }
415
416
    /**
417
     * Make a function that creates objects from a rule.
418
     *
419
     * @param string $nid The normalized ID of the container item.
420
     * @param array $rule The resolved rule for the ID.
421
     * @return \Closure Returns a function that when called will create a new instance of the class.
422
     * @throws NotFoundException No entry was found for this identifier.
423
     */
424 48
    private function makeFactory($nid, array $rule) {
425 48
        $className = empty($rule['class']) ? $nid : $rule['class'];
426
427 48
        if (!empty($rule['factory'])) {
428
            // The instance is created with a user-supplied factory function.
429 6
            $callback = $rule['factory'];
430 6
            $function = $this->reflectCallback($callback);
431
432 6
            if ($function->getNumberOfParameters() > 0) {
433 3
                $callbackArgs = $this->makeDefaultArgs($function, (array)$rule['constructorArgs']);
434
                $factory = function ($args) use ($callback, $callbackArgs) {
435 3
                    return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args));
436 3
                };
437
            } else {
438 3
                $factory = $callback;
439
            }
440
441
            // If a class is specified then still reflect on it so that calls can be made against it.
442 6
            if (class_exists($className)) {
443 6
                $class = new \ReflectionClass($className);
444
            }
445
        } else {
446
            // The instance is created by newing up a class.
447 42
            if (!class_exists($className)) {
448 1
                throw new NotFoundException("Class $className does not exist.", 404);
449
            }
450 41
            $class = new \ReflectionClass($className);
451 41
            $constructor = $class->getConstructor();
452
453 41
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
454 38
                $constructorArgs = $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs']);
455
456
                $factory = function ($args) use ($className, $constructorArgs) {
457 37
                    return new $className(...array_values($this->resolveArgs($constructorArgs, $args)));
458 37
                };
459
            } else {
460
                $factory = function () use ($className) {
461 4
                    return new $className;
462 4
                };
463
            }
464
        }
465
466
        // Add calls to the factory.
467 46
        if (isset($class) && !empty($rule['calls'])) {
468 6
            $calls = [];
469
470
            // Generate the calls array.
471 6
            foreach ($rule['calls'] as $call) {
472 6
                [$methodName, $args] = $call;
473 6
                $method = $class->getMethod($methodName);
474 6
                $calls[] = [$methodName, $this->makeDefaultArgs($method, $args)];
475
            }
476
477
            // Wrap the factory in one that makes the calls.
478
            $factory = function ($args) use ($factory, $calls) {
479 6
                $instance = $factory($args);
480
481 6
                foreach ($calls as $call) {
482 6
                    [$methodName, $defaultArgs] = $call;
483 6
                    $finalArgs = $this->resolveArgs($defaultArgs, [], $instance);
484 6
                    call_user_func_array(
485 6
                        [$instance, $methodName],
486 6
                        $finalArgs
487
                    );
488
                }
489
490 6
                return $instance;
491 6
            };
492
        }
493
494 46
        return $factory;
495
    }
496
497
    /**
498
     * Create a shared instance of a class from a rule.
499
     *
500
     * This method has the side effect of adding the new instance to the internal instances array of this object.
501
     *
502
     * @param string $nid The normalized ID of the container item.
503
     * @param array $rule The resolved rule for the ID.
504
     * @param array $args Additional arguments passed during creation.
505
     * @return object Returns the the new instance.
506
     * @throws NotFoundException Throws an exception if the class does not exist.
507
     */
508 31
    private function createSharedInstance($nid, array $rule, array $args) {
509 31
        if (!empty($rule['factory'])) {
510
            // The instance is created with a user-supplied factory function.
511 3
            $callback = $rule['factory'];
512 3
            $function = $this->reflectCallback($callback);
513
514 3
            if ($function->getNumberOfParameters() > 0) {
515 1
                $callbackArgs = $this->resolveArgs(
516 1
                    $this->makeDefaultArgs($function, (array)$rule['constructorArgs']),
517 1
                    $args
518
                );
519
520 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
521 1
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
522
            } else {
523 2
                $this->instances[$nid] = $instance = $callback();
524
            }
525
526
            // Reflect on the instance so that calls can be made against it.
527 3
            if (is_object($instance)) {
528 3
                $class = new \ReflectionClass(get_class($instance));
529
            }
530
        } else {
531 28
            $className = empty($rule['class']) ? $nid : $rule['class'];
532 28
            if (!class_exists($className)) {
533 1
                throw new NotFoundException("Class $className does not exist.", 404);
534
            }
535 27
            $class = new \ReflectionClass($className);
536 27
            $constructor = $class->getConstructor();
537
538 27
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
539
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
540 26
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
541
542 26
                $constructorArgs = $this->resolveArgs(
543 26
                    $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $rule),
544 25
                    $args
545
                );
546 24
                $constructor->invokeArgs($instance, $constructorArgs);
547
            } else {
548 2
                $this->instances[$nid] = $instance = new $class->name;
549
            }
550
        }
551
552
        // Call subsequent calls on the new object.
553 28
        if (isset($class) && !empty($rule['calls'])) {
554 4
            foreach ($rule['calls'] as $call) {
555 4
                list($methodName, $args) = $call;
556 4
                $method = $class->getMethod($methodName);
557
558 4
                $args = $this->resolveArgs(
559 4
                    $this->makeDefaultArgs($method, $args, $rule),
560 4
                    [],
561 4
                    $instance
562
                );
563
564 4
                $method->invokeArgs($instance, $args);
565
            }
566
        }
567
568 28
        return $instance;
569
    }
570
571
572
    /**
573
     * Find the class implemented by an ID.
574
     *
575
     * This tries to see if a rule exists for a normalized ID and what class it evaluates to.
576
     *
577
     * @param string $nid The normalized ID to look up.
578
     * @return string|null Returns the name of the class associated with the rule or **null** if one could not be found.
579
     */
580 7
    private function findRuleClass($nid) {
581 7
        if (!isset($this->rules[$nid])) {
582 3
            return null;
583 4
        } elseif (!empty($this->rules[$nid]['aliasOf'])) {
584
            return $this->findRuleClass($this->rules[$nid]['aliasOf']);
585 4
        } elseif (!empty($this->rules[$nid]['class'])) {
586 2
            return $this->rules[$nid]['class'];
587
        }
588
589 2
        return null;
590
    }
591
592
    /**
593
     * Make an array of default arguments for a given function.
594
     *
595
     * @param \ReflectionFunctionAbstract $function The function to make the arguments for.
596
     * @param array $ruleArgs An array of default arguments specifically for the function.
597
     * @return array Returns an array in the form `name => defaultValue`.
598
     * @throws NotFoundException If a non-optional class param is reflected and does not exist.
599
     */
600 70
    private function makeDefaultArgs(\ReflectionFunctionAbstract $function, array $ruleArgs) {
601 70
        $ruleArgs = array_change_key_case($ruleArgs);
602 70
        $result = [];
603
604 70
        $pos = 0;
605 70
        foreach ($function->getParameters() as $i => $param) {
606 70
            $name = strtolower($param->name);
607
608 70
            $reflectedClass = null;
0 ignored issues
show
The assignment to $reflectedClass is dead and can be removed.
Loading history...
609
            try {
610 70
                $reflectedClass = $param->getClass();
611 4
            } catch (\ReflectionException $e) {
612
                // If the class is not found in the autoloader a reflection exception is thrown.
613
                // Unless the parameter is optional we will want to rethrow.
614 4
                if (!$param->isOptional()) {
615 2
                    throw new NotFoundException(
616 2
                        "Could not find required constructor param $name in the autoloader.",
617 2
                        500,
618 2
                        $e
619
                    );
620
                }
621
            }
622
623 68
            $hasOrdinalRule = isset($ruleArgs[$pos]);
624
625 68
            $isMatchingOrdinalReference = false;
626 68
            $isMatchingOrdinalInstance = false;
627 68
            if ($hasOrdinalRule && $reflectedClass) {
628 12
                $ordinalRule = $ruleArgs[$pos];
629
630 12
                if ($ordinalRule instanceof Reference) {
631 7
                    $ruleClass = $ordinalRule->getName();
632 7
                    if (($resolvedRuleClass = $this->findRuleClass($ruleClass)) !== null) {
0 ignored issues
show
It seems like $ruleClass can also be of type array; however, parameter $nid of Garden\Container\Container::findRuleClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

632
                    if (($resolvedRuleClass = $this->findRuleClass(/** @scrutinizer ignore-type */ $ruleClass)) !== null) {
Loading history...
633 2
                        $ruleClass = $resolvedRuleClass;
634
                    }
635
636
                    // The argument is a reference that matches the type hint.
637 7
                    $isMatchingOrdinalReference = is_a(
638 7
                        $ruleClass,
639 7
                        $reflectedClass->getName(),
640 7
                        true
641
                    );
642 5
                } elseif (is_object($ordinalRule)) {
643
                    // The argument is an instance that matches the type hint.
644 2
                    $isMatchingOrdinalInstance = is_a($ordinalRule, $reflectedClass->getName());
645
                }
646
            }
647
648 68
            if (array_key_exists($name, $ruleArgs)) {
649 5
                $value = $ruleArgs[$name];
650
            } elseif (
651 66
                $reflectedClass
652 66
                && $hasOrdinalRule
653 66
                && ($isMatchingOrdinalReference|| $isMatchingOrdinalInstance)
654
            ) {
655 7
                $value = $ruleArgs[$pos];
656 7
                $pos++;
657 64
            } elseif ($reflectedClass
658 64
                && ($reflectedClass->isInstantiable() || isset($this->rules[$reflectedClass->name]) || array_key_exists($reflectedClass->name, $this->instances))
659
            ) {
660 17
                $value = new DefaultReference($this->normalizeID($reflectedClass->name));
661 60
            } elseif ($hasOrdinalRule) {
662 21
                $value = $ruleArgs[$pos];
663 21
                $pos++;
664 44
            } elseif ($param->isDefaultValueAvailable()) {
665 37
                $value = $param->getDefaultValue();
666 7
            } elseif ($param->isOptional()) {
667
                $value = null;
668
            } else {
669 7
                $value = new RequiredParameter($param);
670
            }
671
672 68
            $result[$name] = $value;
673
        }
674
675 68
        return $result;
676
    }
677
678
    /**
679
     * Replace an array of default args with called args.
680
     *
681
     * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}.
682
     * @param array $args The arguments passed into a creation.
683
     * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object.
684
     * @return array Returns an array suitable to be applied to a function call.
685
     * @throws MissingArgumentException Throws an exception when a required parameter is missing.
686
     */
687 68
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
688
        // First resolve all passed arguments so their types are known.
689 68
        $args = array_map(
690
            function ($arg) use ($instance) {
691 17
                return $arg instanceof ReferenceInterface ? $arg->resolve($this, $instance) : $arg;
692 68
            },
693 68
            array_change_key_case($args)
694
        );
695
696 68
        $pos = 0;
697 68
        foreach ($defaultArgs as $name => &$default) {
698 68
            if (array_key_exists($name, $args)) {
699
                // This is a named arg and should be used.
700 2
                $value = $args[$name];
701 68
            } elseif (isset($args[$pos]) && (!($default instanceof DefaultReference) || empty($default->getClass()) || is_a($args[$pos], $default->getClass()))) {
702
                // There is an arg at this position and it's the same type as the default arg or the default arg is typeless.
703 15
                $value = $args[$pos];
704 15
                $pos++;
705
            } else {
706
                // There is no passed arg, so use the default arg.
707 57
                $value = $default;
708
            }
709
710 68
            if ($value instanceof ReferenceInterface) {
711 21
                $value = $value->resolve($this, $instance);
712
            }
713
714 66
            $default = $value;
715
        }
716
717 66
        return $defaultArgs;
718
    }
719
720
    /**
721
     * Create an instance of a container item.
722
     *
723
     * This method either creates a new instance or returns an already created shared instance.
724
     *
725
     * @param string $nid The normalized ID of the container item.
726
     * @param array $args Additional arguments to pass to the constructor.
727
     * @return object Returns an object instance.
728
     */
729 77
    private function createInstance($nid, array $args) {
730 77
        $rule = $this->makeRule($nid);
731
732
        // Cache the instance or its factory for future use.
733 77
        if (empty($rule['shared'])) {
734 48
            $factory = $this->makeFactory($nid, $rule);
735 46
            $instance = $factory($args);
736 45
            $this->factories[$nid] = $factory;
737
        } else {
738 31
            $instance = $this->createSharedInstance($nid, $rule, $args);
739
        }
740 71
        return $instance;
741
    }
742
743
    /**
744
     * Call a callback with argument injection.
745
     *
746
     * @param callable $callback The callback to call.
747
     * @param array $args Additional arguments to pass to the callback.
748
     * @return mixed Returns the result of the callback.
749
     * @throws ContainerException Throws an exception if the callback cannot be understood.
750
     */
751 4
    public function call(callable $callback, array $args = []) {
752 4
        $instance = null;
753
754 4
        if (is_array($callback)) {
755 2
            $function = new \ReflectionMethod($callback[0], $callback[1]);
756
757 2
            if (is_object($callback[0])) {
758 2
                $instance = $callback[0];
759
            }
760
        } else {
761 2
            $function = new \ReflectionFunction($callback);
762
        }
763
764 4
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
765
766 4
        return call_user_func_array($callback, $args);
767
    }
768
769
    /**
770
     * Returns true if the container can return an entry for the given identifier. Returns false otherwise.
771
     *
772
     * @param string $id Identifier of the entry to look for.
773
     *
774
     * @return boolean
775
     */
776 5
    public function has($id) {
777 5
        $id = $this->normalizeID($id);
778
779 5
        return isset($this->instances[$id]) || !empty($this->rules[$id]) || class_exists($id);
780
    }
781
782
    /**
783
     * Determines whether a rule has been defined at a given ID.
784
     *
785
     * @param string $id Identifier of the entry to look for.
786
     * @return bool Returns **true** if a rule has been defined or **false** otherwise.
787
     */
788 4
    public function hasRule($id) {
789 4
        $id = $this->normalizeID($id);
790 4
        return !empty($this->rules[$id]);
791
    }
792
793
    /**
794
     * Returns true if the container already has an instance for the given identifier. Returns false otherwise.
795
     *
796
     * @param string $id Identifier of the entry to look for.
797
     *
798
     * @return bool
799
     */
800 1
    public function hasInstance($id) {
801 1
        $id = $this->normalizeID($id);
802
803 1
        return isset($this->instances[$id]);
804
    }
805
806
    /**
807
     * Finds an entry of the container by its identifier and returns it.
808
     *
809
     * @param string $id Identifier of the entry to look for.
810
     *
811
     * @throws NotFoundException  No entry was found for this identifier.
812
     * @throws ContainerException Error while retrieving the entry.
813
     *
814
     * @return mixed Entry.
815
     */
816 69
    public function get($id) {
817 69
        return $this->getArgs($id);
818
    }
819
820
    /**
821
     * Determine the reflection information for a callback.
822
     *
823
     * @param callable $callback The callback to reflect.
824
     * @return \ReflectionFunctionAbstract Returns the reflection function for the callback.
825
     */
826 9
    private function reflectCallback(callable $callback) {
827 9
        if (is_array($callback)) {
828 2
            return new \ReflectionMethod($callback[0], $callback[1]);
829
        } else {
830 7
            return new \ReflectionFunction($callback);
831
        }
832
    }
833
}
834