Completed
Push — master ( 25f213...aa467f )
by Todd
14s queued 11s
created

Container::makeDefaultArgs()   D

Complexity

Conditions 18
Paths 16

Size

Total Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 18.0082

Importance

Changes 0
Metric Value
dl 0
loc 53
ccs 33
cts 34
cp 0.9706
rs 4.8666
c 0
b 0
f 0
cc 18
nc 16
nop 2
crap 18.0082

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 93
    public function __construct() {
26 93
        $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => null]];
27 93
        $this->instances = [];
28 93
        $this->factories = [];
29
30 93
        $this->rule('*');
31 93
    }
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 94
    private function normalizeID($id) {
75 94
        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 93
    public function rule($id) {
94 93
        $id = $this->normalizeID($id);
95
96 93
        if (!isset($this->rules[$id])) {
97 43
            $this->rules[$id] = [];
98
        }
99 93
        $this->currentRuleName = $id;
100 93
        $this->currentRule = &$this->rules[$id];
101
102 93
        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 8
    public function setClass($className) {
121 8
        $this->currentRule['class'] = $className;
122 8
        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
     */
0 ignored issues
show
Documentation introduced by
Should the type for parameter $alias not be string[]?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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 8
                $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 6
                $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 42
    public function setShared($shared) {
253 42
        $this->currentRule['shared'] = $shared;
254 42
        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 25
    public function setConstructorArgs(array $args) {
293 25
        $this->currentRule['constructorArgs'] = $args;
294 25
        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 8
    public function addCall($method, array $args = []) {
320 8
        $this->currentRule['calls'][] = [$method, $args];
321
322 8
        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 74
    public function getArgs($id, array $args = []) {
337 74
        $id = $this->normalizeID($id);
338
339 74
        if (isset($this->instances[$id])) {
340
            // A shared instance just gets returned.
341 18
            return $this->instances[$id];
342
        }
343
344 70
        if (isset($this->factories[$id])) {
345
            // The factory for this object type is already there so call it to create the instance.
346 4
            return $this->factories[$id]($args);
347
        }
348
349 70
        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 70
        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 70
    private function makeRule($nid) {
366 70
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
367
368 70
        if (class_exists($nid)) {
369 62
            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
                    break;
373
                }
374 2
                $rule += $this->rules[$class];
375
            }
376
377
            // Add the default rule.
378 62
            if (!empty($this->rules['*']['inherit'])) {
379 62
                $rule += $this->rules['*'];
380
            }
381
382
            // Add interface calls to the rule.
383 62
            $interfaces = class_implements($nid);
384 62
            foreach ($interfaces as $interface) {
385 41
                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 62
                            $interfaceRule['calls']
404
                        );
405
                    }
406
                }
407
            }
408 12
        } elseif (!empty($this->rules['*']['inherit'])) {
409
            // Add the default rule.
410 12
            $rule += $this->rules['*'];
411
        }
412
413 70
        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 46
    private function makeFactory($nid, array $rule) {
425 46
        $className = empty($rule['class']) ? $nid : $rule['class'];
426
427 46
        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 40
            if (!class_exists($className)) {
448 1
                throw new NotFoundException("Class $className does not exist.", 404);
449
            }
450 39
            $class = new \ReflectionClass($className);
451 39
            $constructor = $class->getConstructor();
452
453 39
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
454 36
                $constructorArgs = $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs']);
455
456
                $factory = function ($args) use ($className, $constructorArgs) {
457 35
                    return new $className(...array_values($this->resolveArgs($constructorArgs, $args)));
458 35
                };
459
            } else {
460
                $factory = function () use ($className) {
461 4
                    return new $className;
462 4
                };
463
            }
464
        }
465
466
        // Add calls to the factory.
467 44
        if (isset($class) && !empty($rule['calls'])) {
468 4
            $calls = [];
469
470
            // Generate the calls array.
471 4
            foreach ($rule['calls'] as $call) {
472 4
                list($methodName, $args) = $call;
473 4
                $method = $class->getMethod($methodName);
474 4
                $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 4
                $instance = $factory($args);
480
481 4
                foreach ($calls as $call) {
482 4
                    call_user_func_array(
483 4
                        [$instance, $call[0]],
484 4
                        $this->resolveArgs($call[1], [], $instance)
485
                    );
486
                }
487
488 4
                return $instance;
489 4
            };
490
        }
491
492 44
        return $factory;
493
    }
494
495
    /**
496
     * Create a shared instance of a class from a rule.
497
     *
498
     * This method has the side effect of adding the new instance to the internal instances array of this object.
499
     *
500
     * @param string $nid The normalized ID of the container item.
501
     * @param array $rule The resolved rule for the ID.
502
     * @param array $args Additional arguments passed during creation.
503
     * @return object Returns the the new instance.
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double|string|nu...boolean|resource|object?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
504
     * @throws NotFoundException Throws an exception if the class does not exist.
505
     */
506 26
    private function createSharedInstance($nid, array $rule, array $args) {
507 26
        if (!empty($rule['factory'])) {
508
            // The instance is created with a user-supplied factory function.
509 3
            $callback = $rule['factory'];
510 3
            $function = $this->reflectCallback($callback);
511
512 3
            if ($function->getNumberOfParameters() > 0) {
513 1
                $callbackArgs = $this->resolveArgs(
514 1
                    $this->makeDefaultArgs($function, (array)$rule['constructorArgs']),
515 1
                    $args
516
                );
517
518 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
519 1
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
520
            } else {
521 2
                $this->instances[$nid] = $instance = $callback();
522
            }
523
524
            // Reflect on the instance so that calls can be made against it.
525 3
            if (is_object($instance)) {
526 3
                $class = new \ReflectionClass(get_class($instance));
527
            }
528
        } else {
529 23
            $className = empty($rule['class']) ? $nid : $rule['class'];
530 23
            if (!class_exists($className)) {
531 1
                throw new NotFoundException("Class $className does not exist.", 404);
532
            }
533 22
            $class = new \ReflectionClass($className);
534 22
            $constructor = $class->getConstructor();
535
536 22
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
537
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
538 21
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
539
540 21
                $constructorArgs = $this->resolveArgs(
541 21
                    $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $rule),
0 ignored issues
show
Unused Code introduced by
The call to Container::makeDefaultArgs() has too many arguments starting with $rule.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
542 20
                    $args
543
                );
544 19
                $constructor->invokeArgs($instance, $constructorArgs);
545
            } else {
546 2
                $this->instances[$nid] = $instance = new $class->name;
547
            }
548
        }
549
550
        // Call subsequent calls on the new object.
551 23
        if (isset($class) && !empty($rule['calls'])) {
552 2
            foreach ($rule['calls'] as $call) {
553 2
                list($methodName, $args) = $call;
554 2
                $method = $class->getMethod($methodName);
555
556 2
                $args = $this->resolveArgs(
557 2
                    $this->makeDefaultArgs($method, $args, $rule),
0 ignored issues
show
Unused Code introduced by
The call to Container::makeDefaultArgs() has too many arguments starting with $rule.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
558 2
                    [],
559 2
                    $instance
560
                );
561
562 2
                $method->invokeArgs($instance, $args);
563
            }
564
        }
565
566 23
        return $instance;
567
    }
568
569
570
    /**
571
     * Find the class implemented by an ID.
572
     *
573
     * This tries to see if a rule exists for a normalized ID and what class it evaluates to.
574
     *
575
     * @param string $nid The normalized ID to look up.
576
     * @return string|null Returns the name of the class associated with the rule or **null** if one could not be found.
577
     */
578 6
    private function findRuleClass($nid) {
579 6
        if (!isset($this->rules[$nid])) {
580 2
            return null;
581 4
        } elseif (!empty($this->rules[$nid]['aliasOf'])) {
582
            return $this->findRuleClass($this->rules[$nid]['aliasOf']);
583 4
        } elseif (!empty($this->rules[$nid]['class'])) {
584 2
            return $this->rules[$nid]['class'];
585
        }
586
587 2
        return null;
588
    }
589
590
    /**
591
     * Make an array of default arguments for a given function.
592
     *
593
     * @param \ReflectionFunctionAbstract $function The function to make the arguments for.
594
     * @param array $ruleArgs An array of default arguments specifically for the function.
595
     * @return array Returns an array in the form `name => defaultValue`.
596
     * @throws NotFoundException If a non-optional class param is reflected and does not exist.
597
     */
598 63
    private function makeDefaultArgs(\ReflectionFunctionAbstract $function, array $ruleArgs) {
599 63
        $ruleArgs = array_change_key_case($ruleArgs);
600 63
        $result = [];
601
602 63
        $pos = 0;
603 63
        foreach ($function->getParameters() as $i => $param) {
604 63
            $name = strtolower($param->name);
605
606 63
            $reflectedClass = null;
607
            try {
608 63
                $reflectedClass = $param->getClass();
609 4
            } catch (\ReflectionException $e) {
610
                // If the class is not found in the autoloader a reflection exception is thrown.
611
                // Unless the parameter is optional we will want to rethrow.
612 4
                if (!$param->isOptional()) {
613 2
                    throw new NotFoundException(
614 2
                        "Could not find required constructor param $name in the autoloader.",
615 2
                        500,
616 2
                        $e
617
                    );
618
                }
619
            }
620
621 61
            if (array_key_exists($name, $ruleArgs)) {
622 3
                $value = $ruleArgs[$name];
623 59
            } elseif ($reflectedClass && isset($ruleArgs[$pos]) &&
624
                // The argument is a reference that matches the type hint.
625 10
                (($ruleArgs[$pos] instanceof Reference && is_a($this->findRuleClass($ruleArgs[$pos]->getName()), $reflectedClass->getName(), true)) ||
0 ignored issues
show
Bug introduced by
Consider using $reflectedClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
626
                // The argument is an instance that matches the type hint.
627 59
                (is_object($ruleArgs[$pos]) && is_a($ruleArgs[$pos], $reflectedClass->name)))
628
            ) {
629 4
                $value = $ruleArgs[$pos];
630 4
                $pos++;
631 59
            } elseif ($reflectedClass
632 59
                && ($reflectedClass->isInstantiable() || isset($this->rules[$reflectedClass->name]) || array_key_exists($reflectedClass->name, $this->instances))
633
            ) {
634 11
                $value = new DefaultReference($this->normalizeID($reflectedClass->name));
635 58
            } elseif (array_key_exists($pos, $ruleArgs)) {
636 22
                $value = $ruleArgs[$pos];
637 22
                $pos++;
638 40
            } elseif ($param->isDefaultValueAvailable()) {
639 33
                $value = $param->getDefaultValue();
640 7
            } elseif ($param->isOptional()) {
641
                $value = null;
642
            } else {
643 7
                $value = new RequiredParameter($param);
644
            }
645
646 61
            $result[$name] = $value;
647
        }
648
649 61
        return $result;
650
    }
651
652
    /**
653
     * Replace an array of default args with called args.
654
     *
655
     * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}.
656
     * @param array $args The arguments passed into a creation.
657
     * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object.
658
     * @return array Returns an array suitable to be applied to a function call.
659
     * @throws MissingArgumentException Throws an exception when a required parameter is missing.
660
     */
661 61
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
662
        // First resolve all passed arguments so their types are known.
663 61
        $args = array_map(
664
            function ($arg) use ($instance) {
665 15
                return $arg instanceof ReferenceInterface ? $arg->resolve($this, $instance) : $arg;
666 61
            },
667 61
            array_change_key_case($args)
668
        );
669
670 61
        $pos = 0;
671 61
        foreach ($defaultArgs as $name => &$default) {
672 61
            if (array_key_exists($name, $args)) {
673
                // This is a named arg and should be used.
674 2
                $value = $args[$name];
675 61
            } elseif (isset($args[$pos]) && (!($default instanceof DefaultReference) || empty($default->getClass()) || is_a($args[$pos], $default->getClass()))) {
676
                // There is an arg at this position and it's the same type as the default arg or the default arg is typeless.
677 13
                $value = $args[$pos];
678 13
                $pos++;
679
            } else {
680
                // There is no passed arg, so use the default arg.
681 50
                $value = $default;
682
            }
683
684 61
            if ($value instanceof ReferenceInterface) {
685 15
                $value = $value->resolve($this, $instance);
686
            }
687
688 59
            $default = $value;
689
        }
690
691 59
        return $defaultArgs;
692
    }
693
694
    /**
695
     * Create an instance of a container item.
696
     *
697
     * This method either creates a new instance or returns an already created shared instance.
698
     *
699
     * @param string $nid The normalized ID of the container item.
700
     * @param array $args Additional arguments to pass to the constructor.
701
     * @return object Returns an object instance.
702
     */
703 70
    private function createInstance($nid, array $args) {
704 70
        $rule = $this->makeRule($nid);
705
706
        // Cache the instance or its factory for future use.
707 70
        if (empty($rule['shared'])) {
708 46
            $factory = $this->makeFactory($nid, $rule);
709 44
            $instance = $factory($args);
710 43
            $this->factories[$nid] = $factory;
711
        } else {
712 26
            $instance = $this->createSharedInstance($nid, $rule, $args);
713
        }
714 64
        return $instance;
715
    }
716
717
    /**
718
     * Call a callback with argument injection.
719
     *
720
     * @param callable $callback The callback to call.
721
     * @param array $args Additional arguments to pass to the callback.
722
     * @return mixed Returns the result of the callback.
723
     * @throws ContainerException Throws an exception if the callback cannot be understood.
724
     */
725 4
    public function call(callable $callback, array $args = []) {
726 4
        $instance = null;
727
728 4
        if (is_array($callback)) {
729 2
            $function = new \ReflectionMethod($callback[0], $callback[1]);
730
731 2
            if (is_object($callback[0])) {
732 2
                $instance = $callback[0];
733
            }
734
        } else {
735 2
            $function = new \ReflectionFunction($callback);
736
        }
737
738 4
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
739
740 4
        return call_user_func_array($callback, $args);
741
    }
742
743
    /**
744
     * Returns true if the container can return an entry for the given identifier. Returns false otherwise.
745
     *
746
     * @param string $id Identifier of the entry to look for.
747
     *
748
     * @return boolean
749
     */
750 5
    public function has($id) {
751 5
        $id = $this->normalizeID($id);
752
753 5
        return isset($this->instances[$id]) || !empty($this->rules[$id]) || class_exists($id);
754
    }
755
756
    /**
757
     * Determines whether a rule has been defined at a given ID.
758
     *
759
     * @param string $id Identifier of the entry to look for.
760
     * @return bool Returns **true** if a rule has been defined or **false** otherwise.
761
     */
762 4
    public function hasRule($id) {
763 4
        $id = $this->normalizeID($id);
764 4
        return !empty($this->rules[$id]);
765
    }
766
767
    /**
768
     * Returns true if the container already has an instance for the given identifier. Returns false otherwise.
769
     *
770
     * @param string $id Identifier of the entry to look for.
771
     *
772
     * @return bool
773
     */
774 1
    public function hasInstance($id) {
775 1
        $id = $this->normalizeID($id);
776
777 1
        return isset($this->instances[$id]);
778
    }
779
780
    /**
781
     * Finds an entry of the container by its identifier and returns it.
782
     *
783
     * @param string $id Identifier of the entry to look for.
784
     *
785
     * @throws NotFoundException  No entry was found for this identifier.
786
     * @throws ContainerException Error while retrieving the entry.
787
     *
788
     * @return mixed Entry.
789
     */
790 62
    public function get($id) {
791 62
        return $this->getArgs($id);
792
    }
793
794
    /**
795
     * Determine the reflection information for a callback.
796
     *
797
     * @param callable $callback The callback to reflect.
798
     * @return \ReflectionFunctionAbstract Returns the reflection function for the callback.
799
     */
800 9
    private function reflectCallback(callable $callback) {
801 9
        if (is_array($callback)) {
802 2
            return new \ReflectionMethod($callback[0], $callback[1]);
803
        } else {
804 7
            return new \ReflectionFunction($callback);
805
        }
806
    }
807
}
808