Container   F
last analyzed

Complexity

Total Complexity 132

Size/Duplication

Total Lines 816
Duplicated Lines 0 %

Test Coverage

Coverage 99.01%

Importance

Changes 9
Bugs 1 Features 1
Metric Value
eloc 283
c 9
b 1
f 1
dl 0
loc 816
ccs 300
cts 303
cp 0.9901
rs 2
wmc 132

38 Methods

Rating   Name   Duplication   Size   Complexity  
A getAliases() 0 10 4
A arrayClone() 0 10 3
A getClass() 0 2 2
A clearInstances() 0 2 1
A setFactory() 0 3 1
A isShared() 0 2 1
A getFactory() 0 2 2
A __clone() 0 3 1
A getConstructorArgs() 0 2 2
A removeAlias() 0 9 3
A getInherit() 0 2 1
A setAliasOf() 0 9 2
A __construct() 0 6 1
A setInherit() 0 3 1
A setInstance() 0 3 1
A setShared() 0 3 1
A defaultRule() 0 2 1
A addCall() 0 4 1
A normalizeID() 0 2 1
A setConstructorArgs() 0 3 1
A setClass() 0 3 1
A getArgs() 0 21 4
A rule() 0 10 2
A addAlias() 0 11 3
A getAliasOf() 0 2 2
A call() 0 16 3
A reflectCallback() 0 5 2
D makeDefaultArgs() 0 76 21
A hasInstance() 0 4 1
B resolveArgs() 0 31 9
A get() 0 2 1
C makeFactory() 0 71 12
A findRuleClass() 0 10 4
A createInstance() 0 12 2
B createSharedInstance() 0 61 11
A hasRule() 0 3 1
A has() 0 4 3
D makeRule() 0 49 19

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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) {
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
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),
0 ignored issues
show
Unused Code introduced by
The call to Garden\Container\Container::makeDefaultArgs() has too many arguments starting with $rule. ( Ignorable by Annotation )

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

543
                    $this->/** @scrutinizer ignore-call */ 
544
                           makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $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. Please note the @ignore annotation hint above.

Loading history...
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
Unused Code introduced by
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
Bug introduced by
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