Completed
Pull Request — master (#6)
by Todd
01:27
created

Container::makeRule()   D

Complexity

Conditions 17
Paths 12

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 17

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 32
cts 32
cp 1
rs 4.9679
c 0
b 0
f 0
cc 17
eloc 24
nc 12
nop 1
crap 17

How to fix   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-2016 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Container;
9
10
use Interop\Container\ContainerInterface;
11
12
/**
13
 * An inversion of control container.
14
 */
15
class Container implements ContainerInterface {
16
    private $currentRule;
17
    private $instances;
18
    private $rules;
19
    private $factories;
20
21
    /**
22
     * Construct a new instance of the {@link Container} class.
23
     */
24 47
    public function __construct() {
25 47
        $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => null]];
26 47
        $this->currentRule = &$this->rules['*'];
27 47
        $this->instances = [];
28 47
        $this->factories = [];
29 47
    }
30
31
    /**
32
     * Normalize a container entry ID.
33
     *
34
     * @param string $id The ID to normalize.
35
     * @return string Returns a normalized ID as a string.
36
     */
37 45
    private function normalizeID($id) {
38 45
        return ltrim($id, '\\');
39
    }
40
41
    /**
42
     * Set the current rule to the default rule.
43
     *
44
     * @return $this
45
     */
46 1
    public function defaultRule() {
47 1
        $this->currentRule = &$this->rules['*'];
48 1
        return $this;
49
    }
50
51
    /**
52
     * Set the current rule.
53
     *
54
     * @param string $id The ID of the rule.
55
     * @return $this
56
     */
57 18
    public function rule($id) {
58 18
        $id = $this->normalizeID($id);
59
60 18
        if (!isset($this->rules[$id])) {
61 18
            $this->rules[$id] = [];
62 18
        }
63 18
        $this->currentRule = &$this->rules[$id];
64 18
        return $this;
65
    }
66
67
    /**
68
     * Get the class name of the current rule.
69
     *
70
     * @return string Returns a class name.
71
     */
72 2
    public function getClass() {
73 2
        return empty($this->currentRule['class']) ? '' : $this->currentRule['class'];
74
    }
75
76
    /**
77
     * Set the name of the class for the current rule.
78
     *
79
     * @param string $value A valid class name.
80
     * @return $this
81
     */
82 4
    public function setClass($value) {
83 4
        $this->currentRule['class'] = $value;
84 4
        return $this;
85
    }
86
87
    /**
88
     * Get the factory callback for the current rule.
89
     *
90
     * @return callable|null Returns the rule's factory or **null** if it has none.
91
     */
92 2
    public function getFactory() {
93 2
        return isset($this->currentRule['factory']) ? $this->currentRule['factory'] : null;
94
    }
95
96
    /**
97
     * Set the factory that will be used to create the instance for the current rule.
98
     *
99
     * @param callable $value This callback will be called to create the instance for the rule.
100
     * @return $this
101
     */
102 10
    public function setFactory(callable $value) {
103 10
        $this->currentRule['factory'] = $value;
104 10
        return $this;
105
    }
106
107
    /**
108
     * Whether or not the current rule is shared.
109
     *
110
     * @return bool Returns **true** if the rule is shared or **false** otherwise.
111
     */
112 2
    public function isShared() {
113 2
        return !empty($this->currentRule['shared']);
114
    }
115
116
    /**
117
     * Set whether or not the current rule is shared.
118
     *
119
     * @param bool $value Whether or not the current rule is shared.
120
     * @return $this
121
     */
122 17
    public function setShared($value) {
123 17
        $this->currentRule['shared'] = $value;
124 17
        return $this;
125
    }
126
127
    /**
128
     * Whether or not the current rule will inherit to subclasses.
129
     *
130
     * @return bool Returns **true** if the current rule inherits or **false** otherwise.
131
     */
132 2
    public function getInherit() {
133 2
        return !empty($this->currentRule['inherit']);
134
    }
135
136
    /**
137
     * Set whether or not the current rule extends to subclasses.
138
     *
139
     * @param bool $value Pass **true** to have subclasses inherit this rule or **false** otherwise.
140
     * @return $this
141
     */
142 3
    public function setInherit($value) {
143 3
        $this->currentRule['inherit'] = $value;
144 3
        return $this;
145
    }
146
147
    /**
148
     * Get the constructor arguments for the current rule.
149
     *
150
     * @return array Returns the constructor arguments for the current rule.
151
     */
152 2
    public function getConstructorArgs() {
153 2
        return empty($this->currentRule['constructorArgs']) ? [] : $this->currentRule['constructorArgs'];
154
    }
155
156
    /**
157
     * Set the constructor arguments for the current rule.
158
     *
159
     * @param array $args An array of constructor arguments.
160
     * @return $this
161
     */
162 15
    public function setConstructorArgs(array $args) {
163 15
        $this->currentRule['constructorArgs'] = $args;
164 15
        return $this;
165
    }
166
167
    /**
168
     * Set a specific shared instance into the container.
169
     *
170
     * When you set an instance into the container then it will always be returned by subsequent retrievals, even if a
171
     * rule is configured that says that instances should not be shared.
172
     *
173
     * @param string $name The name of the container entry.
174
     * @param mixed $instance This instance.
175
     * @return $this
176
     */
177 5
    public function setInstance($name, $instance) {
178 5
        $this->instances[$this->normalizeID($name)] = $instance;
179 5
        return $this;
180
    }
181
182
    /**
183
     * Add a method call to a rule.
184
     *
185
     * @param string $method The name of the method to call.
186
     * @param array $args The arguments to pass to the method.
187
     * @return $this
188
     */
189 6
    public function addCall($method, array $args = []) {
190 6
        $this->currentRule['calls'][] = [$method, $args];
191
192 6
        return $this;
193
    }
194
195
    /**
196
     * Finds an entry of the container by its identifier and returns it.
197
     *
198
     * @param string $id Identifier of the entry to look for.
199
     * @param array $args Additional arguments to pass to the constructor.
200
     *
201
     * @throws NotFoundException No entry was found for this identifier.
202
     * @throws ContainerException Error while retrieving the entry.
203
     *
204
     * @return mixed Entry.
205
     */
206 41
    public function getArgs($id, array $args = []) {
207 41
        $id = $this->normalizeID($id);
208
209 41
        if (isset($this->instances[$id])) {
210
            // A shared instance just gets returned.
211 8
            return $this->instances[$id];
212
        }
213
214 37
        if (isset($this->factories[$id])) {
215
            // The factory for this object type is already there so call it to create the instance.
216 2
            return $this->factories[$id]($args);
217
        }
218
219
        // The factory or instance isn't registered so do that now.
220
        // This call also caches the instance or factory fo faster access next time.
221 37
        return $this->createInstance($id, $args);
222
    }
223
224
    /**
225
     * Make a rule based on an ID.
226
     *
227
     * @param string $nid A normalized ID.
228
     * @return array Returns an array representing a rule.
229
     */
230 37
    private function makeRule($nid) {
231 37
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
232
233 37
        if (class_exists($nid)) {
234 29
            for ($class = get_parent_class($nid); !empty($class); $class = get_parent_class($class)) {
235
                // Don't add the rule if it doesn't say to inherit.
236 6
                if (!isset($this->rules[$class]) || (isset($this->rules[$class]['inherit']) && !$this->rules[$class]['inherit'])) {
237 4
                    break;
238
                }
239 2
                $rule += $this->rules[$class];
240 2
            }
241
242
            // Add the default rule.
243 29
            if (!empty($this->rules['*']['inherit'])) {
244 29
                $rule += $this->rules['*'];
245 29
            }
246
247
            // Add interface calls to the rule.
248 29
            $interfaces = class_implements($nid);
249 29
            foreach ($interfaces as $interface) {
250 22
                if (isset($this->rules[$interface])) {
251 6
                    $interfaceRule = $this->rules[$interface];
252
253 6
                    if (isset($interfaceRule['inherit']) && $interfaceRule['inherit'] === false) {
254 1
                        continue;
255
                    }
256
257 5
                    if (!isset($rule['constructorArgs']) && isset($interfaceRule['constructorArgs'])) {
258 2
                        $rule['constructorArgs'] = $interfaceRule['constructorArgs'];
259 2
                    }
260
261 5
                    if (!empty($interfaceRule['calls'])) {
262 2
                        $rule['calls'] = array_merge(
263 2
                            isset($rule['calls']) ? $rule['calls'] : [],
264 2
                            $interfaceRule['calls']
265 2
                        );
266 2
                    }
267 5
                }
268 29
            }
269 37
        } elseif (!empty($this->rules['*']['inherit'])) {
270
            // Add the default rule.
271 9
            $rule += $this->rules['*'];
272 9
        }
273
274 37
        return $rule;
275
    }
276
277
    /**
278
     * Make a function that creates objects from a rule.
279
     *
280
     * @param string $nid The normalized ID of the container item.
281
     * @param array $rule The resolved rule for the ID.
282
     * @return \Closure Returns a function that when called will create a new instance of the class.
283
     * @throws NotFoundException No entry was found for this identifier.
284
     */
285 29
    private function makeFactory($nid, array $rule) {
286 29
        $className = empty($rule['class']) ? $nid : $rule['class'];
287
288 29
        if (!empty($rule['factory'])) {
289
            // The instance is created with a user-supplied factory function.
290 7
            $callback = $rule['factory'];
291 7
            $function = $this->reflectCallback($callback);
292
293 7
            if ($function->getNumberOfParameters() > 0) {
294 3
                $callbackArgs = $this->makeDefaultArgs($function, (array)$rule['constructorArgs'], $rule);
295
                $factory = function ($args) use ($callback, $callbackArgs) {
296 3
                    return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args));
297 3
                };
298 3
            } else {
299 4
                $factory = $callback;
300
            }
301
302
            // If a class is specified then still reflect on it so that calls can be made against it.
303 7
            if (class_exists($className)) {
304 2
                $class = new \ReflectionClass($className);
305 2
            }
306 7
        } else {
307
            // The instance is created by newing up a class.
308 22
            if (!class_exists($className)) {
309 1
                throw new NotFoundException("Class $className does not exist.", 404);
310
            }
311 21
            $class = new \ReflectionClass($className);
312 21
            $constructor = $class->getConstructor();
313
314 21
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
315 18
                $constructorArgs = $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $rule);
316
317
                $factory = function ($args) use ($class, $constructorArgs) {
318 18
                    return $class->newInstanceArgs($this->resolveArgs($constructorArgs, $args));
319 18
                };
320 18
            } else {
321
                $factory = function () use ($className) {
322 3
                    return new $className;
323 3
                };
324
            }
325
        }
326
327
        // Add calls to the factory.
328 28
        if (isset($class) && !empty($rule['calls'])) {
329 5
            $calls = [];
330
331
            // Generate the calls array.
332 5
            foreach ($rule['calls'] as $call) {
333 5
                list($methodName, $args) = $call;
334 5
                $method = $class->getMethod($methodName);
335 5
                $calls[] = [$methodName, $this->makeDefaultArgs($method, $args, $rule)];
336 5
            }
337
338
            // Wrap the factory in one that makes the calls.
339 5
            $factory = function ($args) use ($factory, $calls) {
340 5
                $instance = $factory($args);
341
342 5
                foreach ($calls as $call) {
343 5
                    call_user_func_array(
344 5
                        [$instance, $call[0]],
345 5
                        $this->resolveArgs($call[1], [], $instance)
346 5
                    );
347 5
                }
348
349 5
                return $instance;
350 5
            };
351 5
        }
352
353 28
        return $factory;
354
    }
355
356
    /**
357
     * Create a shared instance of a class from a rule.
358
     *
359
     * This method has the side effect of adding the new instance to the internal instances array of this object.
360
     *
361
     * @param string $nid The normalized ID of the container item.
362
     * @param array $rule The resolved rule for the ID.
363
     * @param array $args Additional arguments passed during creation.
364
     * @return object Returns the the new instance.
365
     * @throws NotFoundException Throws an exception if the class does not exist.
366
     */
367 9
    private function createSharedInstance($nid, array $rule, array $args) {
368 9
        $className = empty($rule['class']) ? $nid : $rule['class'];
369
370 9
        if (!empty($rule['factory'])) {
371
            // The instance is created with a user-supplied factory function.
372 2
            $callback = $rule['factory'];
373 2
            $function = $this->reflectCallback($callback);
374
375 2
            if ($function->getNumberOfParameters() > 0) {
376 1
                $callbackArgs = $this->resolveArgs(
377 1
                    $this->makeDefaultArgs($function, (array)$rule['constructorArgs'], $rule),
378
                    $args
379 1
                );
380
381 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
382 1
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
383 1
            } else {
384 1
                $this->instances[$nid] = $instance = $callback();
385
            }
386
387
            // If a class is specified then still reflect on it so that calls can be made against it.
388 2
            if (class_exists($className)) {
389
                $class = new \ReflectionClass($className);
390
            }
391 2
        } else {
392 7
            if (!class_exists($className)) {
393 1
                throw new NotFoundException("Class $className does not exist.", 404);
394
            }
395 6
            $class = new \ReflectionClass($className);
396 6
            $constructor = $class->getConstructor();
397
398 6
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
399 5
                $constructorArgs = $this->resolveArgs(
400 5
                    $this->makeDefaultArgs($constructor, (array)$rule['constructorArgs'], $rule),
401
                    $args
402 5
                );
403
404
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
405 5
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
406 5
                $constructor->invokeArgs($instance, $constructorArgs);
407 5
            } else {
408 1
                $this->instances[$nid] = $instance = new $class->name;
409
            }
410
        }
411
412
        // Call subsequent calls on the new object.
413 8
        if (isset($class) && !empty($rule['calls'])) {
414 1
            foreach ($rule['calls'] as $call) {
415 1
                list($methodName, $args) = $call;
416 1
                $method = $class->getMethod($methodName);
417
418 1
                $args = $this->resolveArgs(
419 1
                    $this->makeDefaultArgs($method, $args, $rule),
420 1
                    [],
421
                    $instance
422 1
                );
423
424 1
                $method->invokeArgs($instance, $args);
425 1
            }
426 1
        }
427
428 8
        return $instance;
429
    }
430
431
    /**
432
     * Make an array of default arguments for a given function.
433
     *
434
     * @param \ReflectionFunctionAbstract $function The function to make the arguments for.
435
     * @param array $ruleArgs An array of default arguments specifically for the function.
436
     * @param array $rule The entire rule.
437
     * @return array Returns an array in the form `name => defaultValue`.
438
     */
439 30
    private function makeDefaultArgs(\ReflectionFunctionAbstract $function, array $ruleArgs, array $rule = []) {
0 ignored issues
show
Unused Code introduced by
The parameter $rule is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
440 30
        $ruleArgs = array_change_key_case($ruleArgs);
441 30
        $result = [];
442
443 30
        $pos = 0;
444 30
        foreach ($function->getParameters() as $i => $param) {
445 30
            $name = strtolower($param->name);
446
447 30
            if (array_key_exists($name, $ruleArgs)) {
448 1
                $value = $ruleArgs[$name];
449 30
            } elseif ($param->getClass() && !(isset($ruleArgs[$pos]) && is_object($ruleArgs[$pos]) && get_class($ruleArgs[$pos]) === $param->getClass()->getName())) {
450 5
                $value = new DefaultReference($this->normalizeID($param->getClass()->getName()));
451 29
            } elseif (array_key_exists($pos, $ruleArgs)) {
452 15
                $value = $ruleArgs[$pos];
453 15
                $pos++;
454 29
            } elseif ($param->isDefaultValueAvailable()) {
455 15
                $value = $param->getDefaultValue();
456 15
            } else {
457 1
                $value = null;
458
            }
459
460 30
            $result[$name] = $value;
461 30
        }
462
463 30
        return $result;
464
    }
465
466
    /**
467
     * Replace an array of default args with called args.
468
     *
469
     * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}.
470
     * @param array $args The arguments passed into a creation.
471
     * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object.
472
     * @return array Returns an array suitable to be applied to a function call.
473
     */
474 30
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
475 30
        $args = array_change_key_case($args);
476
477 30
        $pos = 0;
478 30
        foreach ($defaultArgs as $name => &$arg) {
479 30
            if (array_key_exists($name, $args)) {
480
                // This is a named arg and should be used.
481 2
                $value = $args[$name];
482 30
            } elseif (isset($args[$pos]) && (!($arg instanceof DefaultReference) || is_a($args[$pos], $arg->getName()))) {
483
                // There is an arg at this position and it's the same type as the default arg or the default arg is typeless.
484 4
                $value = $args[$pos];
485 4
                $pos++;
486 4
            } else {
487
                // There is no passed arg, so use the default arg.
488 27
                $value = $arg;
489
            }
490
491 30
            if ($value instanceof ReferenceInterface) {
492 5
                $value = $value->resolve($this, $instance);
493 5
            }
494 30
            $arg = $value;
495 30
        }
496
497 30
        return $defaultArgs;
498
    }
499
500
    /**
501
     * Create an instance of a container item.
502
     *
503
     * This method either creates a new instance or returns an already created shared instance.
504
     *
505
     * @param string $nid The normalized ID of the container item.
506
     * @param array $args Additional arguments to pass to the constructor.
507
     * @return object Returns an object instance.
508
     */
509 37
    private function createInstance($nid, array $args) {
510 37
        $rule = $this->makeRule($nid);
511
512
        // Cache the instance or its factory for future use.
513 37
        if (empty($rule['shared'])) {
514 29
            $factory = $this->makeFactory($nid, $rule);
515 28
            $instance = $factory($args);
516 28
            $this->factories[$nid] = $factory;
517 28
        } else {
518 9
            $instance = $this->createSharedInstance($nid, $rule, $args);
519
        }
520 35
        return $instance;
521
    }
522
523
    /**
524
     * Call a callback with argument injection.
525
     *
526
     * @param callable $callback The callback to call.
527
     * @param array $args Additional arguments to pass to the callback.
528
     * @return mixed Returns the result of the callback.
529
     * @throws ContainerException Throws an exception if the callback cannot be understood.
530
     */
531 1
    public function call(callable $callback, array $args = []) {
532 1
        $instance = null;
533 1
        if (is_string($callback) || $callback instanceof \Closure) {
534
            $function = new \ReflectionFunction($callback);
535 1
        } elseif (is_array($callback)) {
536 1
            $function = new \ReflectionMethod($callback[0], $callback[1]);
537
538 1
            if (is_object($callback[0])) {
539 1
                $instance = $callback[0];
540 1
            }
541 1
        } else {
542
            throw new ContainerException("Could not understand callback.", 500);
543
        }
544
545 1
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
546
547 1
        return call_user_func_array($callback, $args);
548
    }
549
550
    /**
551
     * Returns true if the container can return an entry for the given identifier. Returns false otherwise.
552
     *
553
     * @param string $id Identifier of the entry to look for.
554
     *
555
     * @return boolean
556
     */
557
    public function has($id) {
558
        $id = $this->normalizeID($id);
559
560
        return isset($this->instances[$id]) || isset($this->rules[$id]) || class_exists($id);
561
    }
562
563
    /**
564
     * Determines whether a rule has been defined at a given ID.
565
     *
566
     * @param string $id Identifier of the entry to look for.
567
     * @return bool Returns **true** if a rule has been defined or **false** otherwise.
568
     */
569 4
    public function hasRule($id) {
570 4
        $id = $this->normalizeID($id);
571 4
        return !empty($this->rules[$id]);
572
    }
573
574
    /**
575
     * Finds an entry of the container by its identifier and returns it.
576
     *
577
     * @param string $id Identifier of the entry to look for.
578
     *
579
     * @throws NotFoundException  No entry was found for this identifier.
580
     * @throws ContainerException Error while retrieving the entry.
581
     *
582
     * @return mixed Entry.
583
     */
584 36
    public function get($id) {
585 36
        return $this->getArgs($id);
586
    }
587
588
    /**
589
     * Determine the reflection information for a callback.
590
     *
591
     * @param callable $callback The callback to reflect.
592
     * @return \ReflectionFunctionAbstract Returns the reflection function for the callback.
593
     */
594 9
    private function reflectCallback(callable $callback) {
595 9
        if (is_array($callback)) {
596 2
            return new \ReflectionMethod($callback[0], $callback[1]);
597
        } else {
598 7
            return new \ReflectionFunction($callback);
599
        }
600
    }
601
}
602