Completed
Push — master ( c27261...76e12c )
by Todd
8s
created

Container::getConstructorArgs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 0
crap 2
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 39
    public function __construct() {
25 39
        $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => []]];
26 39
        $this->currentRule = &$this->rules['*'];
27 39
        $this->instances = [];
28 39
        $this->factories = [];
29 39
    }
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 37
    private function normalizeID($id) {
38 37
        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 11
    public function rule($id) {
58 11
        $id = $this->normalizeID($id);
59
60 11
        if (!isset($this->rules[$id])) {
61 11
            $this->rules[$id] = [];
62 11
        }
63 11
        $this->currentRule = &$this->rules[$id];
64 11
        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 2
    public function setClass($value) {
83 2
        $this->currentRule['class'] = $value;
84 2
        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 15
    public function setShared($value) {
123 15
        $this->currentRule['shared'] = $value;
124 15
        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 2
    public function setInherit($value) {
143 2
        $this->currentRule['inherit'] = $value;
144 2
        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 11
    public function setConstructorArgs(array $args) {
163 11
        $this->currentRule['constructorArgs'] = $args;
164 11
        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 37
    public function getArgs($id, array $args = []) {
207 37
        $id = $this->normalizeID($id);
208
209 37
        if (isset($this->instances[$id])) {
210
            // A shared instance just gets returned.
211 8
            return $this->instances[$id];
212
        }
213
214 33
        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 33
        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 33
    private function makeRule($nid) {
231 33
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
232
233 33
        if (class_exists($nid)) {
234 25
            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 2
                if (!isset($this->rules[$class]) || (isset($this->rules[$class]['inherit']) && !$this->rules[$class]['inherit'])) {
237 1
                    break;
238
                }
239 1
                $rule += $this->rules[$class];
240 1
            }
241
242
            // Add the default rule.
243 25
            if (!empty($this->rules['*']['inherit'])) {
244 25
                $rule += $this->rules['*'];
245 25
            }
246
247
            // Add interface calls to the rule.
248 25
            $interfaces = class_implements($nid);
249 25
            foreach ($interfaces as $interface) {
250 6
                if (!empty($this->rules[$interface]['calls'])
251 6
                    && (!isset($this->rules[$interface]['inherit']) || $this->rules[$interface]['inherit'] !== false)
252 6
                ) {
253
254 2
                    $rule['calls'] = array_merge(
255 2
                        isset($rule['calls']) ? $rule['calls'] : [],
256 2
                        $this->rules[$interface]['calls']
257 2
                    );
258 2
                }
259 25
            }
260 33
        } elseif (!empty($this->rules['*']['inherit'])) {
261
            // Add the default rule.
262 9
            $rule += $this->rules['*'];
263 9
        }
264
265 33
        return $rule;
266
    }
267
268
    /**
269
     * Make a function that creates objects from a rule.
270
     *
271
     * @param string $nid The normalized ID of the container item.
272
     * @param array $rule The resolved rule for the ID.
273
     * @return \Closure Returns a function that when called will create a new instance of the class.
274
     * @throws NotFoundException No entry was found for this identifier.
275
     */
276 25
    private function makeFactory($nid, array $rule) {
277 25
        $className = empty($rule['class']) ? $nid : $rule['class'];
278
279 25
        if (!empty($rule['factory'])) {
280
            // The instance is created with a user-supplied factory function.
281 7
            $callback = $rule['factory'];
282 7
            $function = $this->reflectCallback($callback);
283
284 7
            if ($function->getNumberOfParameters() > 0) {
285 3
                $callbackArgs = $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule);
286
                $factory = function ($args) use ($callback, $callbackArgs) {
287 3
                    return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args));
288 3
                };
289 3
            } else {
290 4
                $factory = $callback;
291
            }
292
293
            // If a class is specified then still reflect on it so that calls can be made against it.
294 7
            if (class_exists($className)) {
295 2
                $class = new \ReflectionClass($className);
296 2
            }
297 7
        } else {
298
            // The instance is created by newing up a class.
299 18
            if (!class_exists($className)) {
300 1
                throw new NotFoundException("Class $className does not exist.", 404);
301
            }
302 17
            $class = new \ReflectionClass($className);
303 17
            $constructor = $class->getConstructor();
304
305 17
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
306 14
                $constructorArgs = $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule);
307
308
                $factory = function ($args) use ($class, $constructorArgs) {
309 14
                    return $class->newInstanceArgs($this->resolveArgs($constructorArgs, $args));
310 14
                };
311 14
            } else {
312
                $factory = function () use ($className) {
313 3
                    return new $className;
314 3
                };
315
            }
316
        }
317
318
        // Add calls to the factory.
319 24
        if (isset($class) && !empty($rule['calls'])) {
320 5
            $calls = [];
321
322
            // Generate the calls array.
323 5
            foreach ($rule['calls'] as $call) {
324 5
                list($methodName, $args) = $call;
325 5
                $method = $class->getMethod($methodName);
326 5
                $calls[] = [$methodName, $this->makeDefaultArgs($method, $args, $rule)];
327 5
            }
328
329
            // Wrap the factory in one that makes the calls.
330 5
            $factory = function ($args) use ($factory, $calls) {
331 5
                $instance = $factory($args);
332
333 5
                foreach ($calls as $call) {
334 5
                    call_user_func_array(
335 5
                        [$instance, $call[0]],
336 5
                        $this->resolveArgs($call[1], [], $instance)
337 5
                    );
338 5
                }
339
340 5
                return $instance;
341 5
            };
342 5
        }
343
344 24
        return $factory;
345
    }
346
347
    /**
348
     * Create a shared instance of a class from a rule.
349
     *
350
     * This method has the side effect of adding the new instance to the internal instances array of this object.
351
     *
352
     * @param string $nid The normalized ID of the container item.
353
     * @param array $rule The resolved rule for the ID.
354
     * @param array $args Additional arguments passed during creation.
355
     * @return object Returns the the new instance.
356
     * @throws NotFoundException Throws an exception if the class does not exist.
357
     */
358 9
    private function createSharedInstance($nid, array $rule, array $args) {
359 9
        $className = empty($rule['class']) ? $nid : $rule['class'];
360
361 9
        if (!empty($rule['factory'])) {
362
            // The instance is created with a user-supplied factory function.
363 2
            $callback = $rule['factory'];
364 2
            $function = $this->reflectCallback($callback);
365
366 2
            if ($function->getNumberOfParameters() > 0) {
367 1
                $callbackArgs = $this->resolveArgs(
368 1
                    $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule),
369
                    $args
370 1
                );
371
372 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
373 1
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
374 1
            } else {
375 1
                $this->instances[$nid] = $instance = $callback();
376
            }
377
378
            // If a class is specified then still reflect on it so that calls can be made against it.
379 2
            if (class_exists($className)) {
380
                $class = new \ReflectionClass($className);
381
            }
382 2
        } else {
383 7
            if (!class_exists($className)) {
384 1
                throw new NotFoundException("Class $className does not exist.", 404);
385
            }
386 6
            $class = new \ReflectionClass($className);
387 6
            $constructor = $class->getConstructor();
388
389 6
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
390 5
                $constructorArgs = $this->resolveArgs(
391 5
                    $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule),
392
                    $args
393 5
                );
394
395
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
396 5
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
397 5
                $constructor->invokeArgs($instance, $constructorArgs);
398 5
            } else {
399 1
                $this->instances[$nid] = $instance = new $class->name;
400
            }
401
        }
402
403
        // Call subsequent calls on the new object.
404 8
        if (isset($class) && !empty($rule['calls'])) {
405 1
            foreach ($rule['calls'] as $call) {
406 1
                list($methodName, $args) = $call;
407 1
                $method = $class->getMethod($methodName);
408
409 1
                $args = $this->resolveArgs(
410 1
                    $this->makeDefaultArgs($method, $args, $rule),
411 1
                    [],
412
                    $instance
413 1
                );
414
415 1
                $method->invokeArgs($instance, $args);
416 1
            }
417 1
        }
418
419 8
        return $instance;
420
    }
421
422
    /**
423
     * Make an array of default arguments for a given function.
424
     *
425
     * @param \ReflectionFunctionAbstract $function The function to make the arguments for.
426
     * @param array $ruleArgs An array of default arguments specifically for the function.
427
     * @param array $rule The entire rule.
428
     * @return array Returns an array in the form `name => defaultValue`.
429
     */
430 26
    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...
431 26
        $ruleArgs = array_change_key_case($ruleArgs);
432 26
        $result = [];
433
434 26
        $pos = 0;
435 26
        foreach ($function->getParameters() as $i => $param) {
436 26
            $name = strtolower($param->name);
437
438 26
            if (array_key_exists($name, $ruleArgs)) {
439 1
                $value = $ruleArgs[$name];
440 26
            } elseif ($param->getClass() && !(isset($ruleArgs[$pos]) && is_object($ruleArgs[$pos]) && get_class($ruleArgs[$pos]) === $param->getClass()->getName())) {
441 5
                $value = new DefaultReference($this->normalizeID($param->getClass()->getName()));
442 25
            } elseif (array_key_exists($pos, $ruleArgs)) {
443 12
                $value = $ruleArgs[$pos];
444 12
                $pos++;
445 25
            } elseif ($param->isDefaultValueAvailable()) {
446 14
                $value = $param->getDefaultValue();
447 14
            } else {
448 1
                $value = null;
449
            }
450
451 26
            $result[$name] = $value;
452 26
        }
453
454 26
        return $result;
455
    }
456
457
    /**
458
     * Replace an array of default args with called args.
459
     *
460
     * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}.
461
     * @param array $args The arguments passed into a creation.
462
     * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object.
463
     * @return array Returns an array suitable to be applied to a function call.
464
     */
465 26
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
466 26
        $args = array_change_key_case($args);
467
468 26
        $pos = 0;
469 26
        foreach ($defaultArgs as $name => &$arg) {
470 26
            if (array_key_exists($name, $args)) {
471
                // This is a named arg and should be used.
472 2
                $value = $args[$name];
473 26
            } elseif (isset($args[$pos]) && (!($arg instanceof DefaultReference) || is_a($args[$pos], $arg->getName()))) {
474
                // There is an arg at this position and it's the same type as the default arg or the default arg is typeless.
475 4
                $value = $args[$pos];
476 4
                $pos++;
477 4
            } else {
478
                // There is no passed arg, so use the default arg.
479 23
                $value = $arg;
480
            }
481
482 26
            if ($value instanceof ReferenceInterface) {
483 5
                $value = $value->resolve($this, $instance);
484 5
            }
485 26
            $arg = $value;
486 26
        }
487
488 26
        return $defaultArgs;
489
    }
490
491
    /**
492
     * Create an instance of a container item.
493
     *
494
     * This method either creates a new instance or returns an already created shared instance.
495
     *
496
     * @param string $nid The normalized ID of the container item.
497
     * @param array $args Additional arguments to pass to the constructor.
498
     * @return object Returns an object instance.
499
     */
500 33
    private function createInstance($nid, array $args) {
501 33
        $rule = $this->makeRule($nid);
502
503
        // Cache the instance or its factory for future use.
504 33
        if (empty($rule['shared'])) {
505 25
            $factory = $this->makeFactory($nid, $rule);
506 24
            $instance = $factory($args);
507 24
            $this->factories[$nid] = $factory;
508 24
        } else {
509 9
            $instance = $this->createSharedInstance($nid, $rule, $args);
510
        }
511 31
        return $instance;
512
    }
513
514
    /**
515
     * Call a callback with argument injection.
516
     *
517
     * @param callable $callback The callback to call.
518
     * @param array $args Additional arguments to pass to the callback.
519
     * @return mixed Returns the result of the callback.
520
     * @throws ContainerException Throws an exception if the callback cannot be understood.
521
     */
522 1
    public function call(callable $callback, array $args = []) {
523 1
        $instance = null;
524 1
        if (is_string($callback) || $callback instanceof \Closure) {
525
            $function = new \ReflectionFunction($callback);
526 1
        } elseif (is_array($callback)) {
527 1
            $function = new \ReflectionMethod($callback[0], $callback[1]);
528
529 1
            if (is_object($callback[0])) {
530 1
                $instance = $callback[0];
531 1
            }
532 1
        } else {
533
            throw new ContainerException("Could not understand callback.", 500);
534
        }
535
536 1
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
537
538 1
        return call_user_func_array($callback, $args);
539
    }
540
541
    /**
542
     * Returns true if the container can return an entry for the given identifier. Returns false otherwise.
543
     *
544
     * @param string $id Identifier of the entry to look for.
545
     *
546
     * @return boolean
547
     */
548
    public function has($id) {
549
        $id = $this->normalizeID($id);
550
551
        return isset($this->instances[$id]) || isset($this->rules[$id]) || class_exists($id);
552
    }
553
554
    /**
555
     * Finds an entry of the container by its identifier and returns it.
556
     *
557
     * @param string $id Identifier of the entry to look for.
558
     *
559
     * @throws NotFoundException  No entry was found for this identifier.
560
     * @throws ContainerException Error while retrieving the entry.
561
     *
562
     * @return mixed Entry.
563
     */
564 32
    public function get($id) {
565 32
        return $this->getArgs($id);
566
    }
567
568
    /**
569
     * Determine the reflection information for a callback.
570
     *
571
     * @param callable $callback The callback to reflect.
572
     * @return \ReflectionFunctionAbstract Returns the reflection function for the callback.
573
     */
574 9
    private function reflectCallback(callable $callback) {
575 9
        if (is_array($callback)) {
576 2
            return new \ReflectionMethod($callback[0], $callback[1]);
577
        } else {
578 7
            return new \ReflectionFunction($callback);
579
        }
580
    }
581
}
582