Completed
Pull Request — master (#3)
by Todd
01:29
created

Container::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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 36
    public function __construct() {
25 36
        $this->rules = ['*' => ['inherit' => true, 'constructorArgs' => []]];
26 36
        $this->currentRule = &$this->rules['*'];
27 36
        $this->instances = [];
28 36
        $this->factories = [];
29 36
    }
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
    private function normalizeID($id) {
38
        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 10
    public function rule($id) {
58
        $id = $this->normalizeID($id);
59
60 10
        if (!isset($this->rules[$id])) {
61 10
            $this->rules[$id] = [];
62
        }
63 10
        $this->currentRule = &$this->rules[$id];
64 10
        return $this;
65
    }
66
67
    /**
68
     * Set the name of the class for the current rule.
69
     *
70
     * @param string $value A valid class name.
71
     * @return $this
72
     */
73 1
    public function setClass($value) {
74 1
        $this->currentRule['class'] = $value;
75 1
        return $this;
76
    }
77
78
    /**
79
     * Set the factory that will be used to create the instance for the current rule.
80
     *
81
     * @param callable $value This callback will be called to create the instance for the rule.
82
     * @return $this
83
     */
84 9
    public function setFactory(callable $value) {
85 9
        $this->currentRule['factory'] = $value;
86 9
        return $this;
87 9
    }
88
89
    /**
90
     * Set whether or not the current rule is shared.
91
     *
92
     * @param bool $value Whether or not the current rule is shared.
93
     * @return $this
94
     */
95 14
    public function setShared($value) {
96 14
        $this->currentRule['shared'] = $value;
97 14
        return $this;
98
    }
99
100
    /**
101
     * Set whether or not the current rule extends to subclasses.
102
     *
103
     * @param bool $value Pass **true** to have subclasses inherit this rule or **false** otherwise.
104
     * @return $this
105
     */
106 1
    public function setInherit($value) {
107 1
        $this->currentRule['inherit'] = $value;
108 1
        return $this;
109
    }
110
111
    /**
112
     * Set the constructor arguments for the current rule.
113
     *
114
     * @param array $args An array of constructor arguments.
115
     * @return $this
116
     */
117 10
    public function setConstructorArgs(array $args) {
118 10
        $this->currentRule['constructorArgs'] = $args;
119 10
        return $this;
120 10
    }
121
122
    /**
123
     * Set a specific shared instance into the container.
124
     *
125
     * When you set an instance into the container then it will always be returned by subsequent retrievals, even if a
126
     * rule is configured that says that instances should not be shared.
127
     *
128
     * @param string $name The name of the container entry.
129
     * @param mixed $instance This instance.
130
     * @return $this
131
     */
132 3
    public function setInstance($name, $instance) {
133
        $this->instances[$this->normalizeID($name)] = $instance;
134 3
        return $this;
135
    }
136
137
    /**
138
     * Add a method call to a rule.
139
     *
140
     * @param string $method The name of the method to call.
141
     * @param array $args The arguments to pass to the method.
142
     * @return $this
143
     */
144 3
    public function addCall($method, array $args = []) {
145 3
        $this->currentRule['calls'][] = [$method, $args];
146
147 3
        return $this;
148 3
    }
149
150
    /**
151
     * Finds an entry of the container by its identifier and returns it.
152
     *
153
     * @param string $id Identifier of the entry to look for.
154
     * @param array $args Additional arguments to pass to the constructor.
155
     *
156
     * @throws NotFoundException No entry was found for this identifier.
157
     * @throws ContainerException Error while retrieving the entry.
158
     *
159
     * @return mixed Entry.
160
     */
161 28
    public function getArgs($id, array $args = []) {
162
        $id = $this->normalizeID($id);
163
164 28
        if (isset($this->instances[$id])) {
165
            // A shared instance just gets returned.
166 6
            return $this->instances[$id];
167
        }
168
169 28
        if (isset($this->factories[$id])) {
170
            // The factory for this object type is already there so call it to create the instance.
171
            return $this->factories[$id]($args);
172
        }
173
174
        // The factory or instance isn't registered so do that now.
175
        // This call also caches the instance or factory fo faster access next time.
176 2
        return $this->createInstance($id, $args);
177 28
    }
178
179
    /**
180
     * Make a rule based on an ID.
181
     *
182
     * @param string $nid A normalized ID.
183
     * @return array Returns an array representing a rule.
184
     */
185 30
    private function makeRule($nid) {
186 30
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
187
188
        if (class_exists($nid)) {
189
            for ($class = get_parent_class($nid); !empty($class); $class = get_parent_class($class)) {
190
                // Don't add the rule if it doesn't say to inherit.
191 2
                if (!isset($this->rules[$class]) || (isset($this->rules[$class]['inherit']) && !$this->rules[$class]['inherit'])) {
192 1
                    break;
193
                }
194 1
                $rule += $this->rules[$class];
195 22
            }
196
197
            // Add the default rule.
198 23
            if (!empty($this->rules['*']['inherit'])) {
199 23
                $rule += $this->rules['*'];
200
            }
201
202
            // Add interface calls to the rule.
203
            $interfaces = class_implements($nid);
204
            foreach ($interfaces as $interface) {
205 6
                if (!empty($this->rules[$interface]['calls'])
206 2
                    && (!isset($this->rules[$interface]['inherit']) || $this->rules[$interface]['inherit'] !== false)
207
                ) {
208
209
                    $rule['calls'] = array_merge(
210 2
                        isset($rule['calls']) ? $rule['calls'] : [],
211 2
                        $this->rules[$interface]['calls']
212
                    );
213
                }
214 17
            }
215 9
        } elseif (!empty($this->rules['*']['inherit'])) {
216
            // Add the default rule.
217 9
            $rule += $this->rules['*'];
218 23
        }
219
220 30
        return $rule;
221
    }
222
223
    /**
224
     * Make a function that creates objects from a rule.
225
     *
226
     * @param string $nid The normalized ID of the container item.
227
     * @param array $rule The resolved rule for the ID.
228
     * @return \Closure Returns a function that when called will create a new instance of the class.
229
     * @throws NotFoundException No entry was found for this identifier.
230
     */
231 23
    private function makeFactory($nid, array $rule) {
232 23
        $className = empty($rule['class']) ? $nid : $rule['class'];
233
234 23
        if (!empty($rule['factory'])) {
235
            // The instance is created with a user-supplied factory function.
236 7
            $callback = $rule['factory'];
237
            $function = $this->reflectCallback($callback);
238
239
            if ($function->getNumberOfParameters() > 0) {
240
                $callbackArgs = $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule);
241
                $factory = function ($args) use ($callback, $callbackArgs) {
242
                    return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args));
243 3
                };
244
            } else {
245 4
                $factory = $callback;
246 3
            }
247
248
            // If a class is specified then still reflect on it so that calls can be made against it.
249
            if (class_exists($className)) {
250
                $class = new \ReflectionClass($className);
251
            }
252
        } else {
253
            // The instance is created by newing up a class.
254
            if (!class_exists($className)) {
255
                throw new NotFoundException("Class $className does not exist.", 404);
256
            }
257
            $class = new \ReflectionClass($className);
258
            $constructor = $class->getConstructor();
259
260 3
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
261
                $constructorArgs = $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule);
262
263
                $factory = function ($args) use ($class, $constructorArgs) {
264
                    return $class->newInstanceArgs($this->resolveArgs($constructorArgs, $args));
265 12
                };
266
            } else {
267
                $factory = function () use ($className) {
268
                    return new $className;
269 3
                };
270 12
            }
271 7
        }
272
273
        // Add calls to the factory.
274 22
        if (isset($class) && !empty($rule['calls'])) {
275 5
            $calls = [];
276
277
            // Generate the calls array.
278 5
            foreach ($rule['calls'] as $call) {
279 2
                list($methodName, $args) = $call;
280
                $method = $class->getMethod($methodName);
281
                $calls[] = [$methodName, $this->makeDefaultArgs($method, $args, $rule)];
282
            }
283
284
            // Wrap the factory in one that makes the calls.
285 5
            $factory = function ($args) use ($factory, $calls) {
286
                $instance = $factory($args);
287
288
                foreach ($calls as $call) {
289
                    call_user_func_array(
290 2
                        [$instance, $call[0]],
291 2
                        $this->resolveArgs($call[1], [], $instance)
292
                    );
293
                }
294
295 5
                return $instance;
296 5
            };
297
        }
298
299 22
        return $factory;
300 23
    }
301
302
    /**
303
     * Create a shared instance of a class from a rule.
304
     *
305
     * This method has the side effect of adding the new instance to the internal instances array of this object.
306
     *
307
     * @param string $nid The normalized ID of the container item.
308
     * @param array $rule The resolved rule for the ID.
309
     * @param array $args Additional arguments passed during creation.
310
     * @return object Returns the the new instance.
311
     * @throws NotFoundException Throws an exception if the class does not exist.
312
     */
313 9
    private function createSharedInstance($nid, array $rule, array $args) {
314 9
        $className = empty($rule['class']) ? $nid : $rule['class'];
315
316 9
        if (!empty($rule['factory'])) {
317
            // The instance is created with a user-supplied factory function.
318 2
            $callback = $rule['factory'];
319
            $function = $this->reflectCallback($callback);
320
321
            if ($function->getNumberOfParameters() > 0) {
322 1
                $callbackArgs = $this->resolveArgs(
323 1
                    $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule),
324
                    $args
325
                );
326
327 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
328
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
329
            } else {
330
                $this->instances[$nid] = $instance = $callback();
331 1
            }
332
333
            // If a class is specified then still reflect on it so that calls can be made against it.
334
            if (class_exists($className)) {
335
                $class = new \ReflectionClass($className);
336
            }
337
        } else {
338
            if (!class_exists($className)) {
339
                throw new NotFoundException("Class $className does not exist.", 404);
340
            }
341
            $class = new \ReflectionClass($className);
342
            $constructor = $class->getConstructor();
343
344 1
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
345 5
                $constructorArgs = $this->resolveArgs(
346 5
                    $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule),
347
                    $args
348
                );
349
350
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
351
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
352
                $constructor->invokeArgs($instance, $constructorArgs);
353
            } else {
354
                $this->instances[$nid] = $instance = new $class->name;
355 5
            }
356 2
        }
357
358
        // Call subsequent calls on the new object.
359 8
        if (isset($class) && !empty($rule['calls'])) {
360 1
            foreach ($rule['calls'] as $call) {
361 1
                list($methodName, $args) = $call;
362
                $method = $class->getMethod($methodName);
363
364 1
                $args = $this->resolveArgs(
365 1
                    $this->makeDefaultArgs($method, $args, $rule),
366 1
                    [],
367
                    $instance
368
                );
369
370
                $method->invokeArgs($instance, $args);
371
            }
372
        }
373
374 8
        return $instance;
375 9
    }
376
377
    /**
378
     * Make an array of default arguments for a given function.
379
     *
380
     * @param \ReflectionFunctionAbstract $function The function to make the arguments for.
381
     * @param array $ruleArgs An array of default arguments specifically for the function.
382
     * @param array $rule The entire rule.
383
     * @return array Returns an array in the form `name => defaultValue`.
384
     */
385 18
    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...
386
        $ruleArgs = array_change_key_case($ruleArgs);
387 18
        $result = [];
388
389 18
        $pos = 0;
390 18
        foreach ($function->getParameters() as $i => $param) {
391
            $name = strtolower($param->name);
392
393
            if (array_key_exists($name, $ruleArgs)) {
394 1
                $value = $ruleArgs[$name];
395
            } elseif ($param->getClass() && !(isset($ruleArgs[$pos]) && is_object($ruleArgs[$pos]) && get_class($ruleArgs[$pos]) === $param->getClass()->getName())) {
396
                $value = new DefaultReference($this->normalizeID($param->getClass()->getName()));
397
            } elseif (array_key_exists($pos, $ruleArgs)) {
398 7
                $value = $ruleArgs[$pos];
399 7
                $pos++;
400
            } elseif ($param->isDefaultValueAvailable()) {
401
                $value = $param->getDefaultValue();
402
            } else {
403 1
                $value = null;
404 12
            }
405
406 13
            $result[$name] = $value;
407
        }
408
409 18
        return $result;
410 18
    }
411
412
    /**
413
     * Replace an array of default args with called args.
414
     *
415
     * @param array $defaultArgs The default arguments from {@link Container::makeDefaultArgs()}.
416
     * @param array $args The arguments passed into a creation.
417
     * @param mixed $instance An object instance if the arguments are being resolved on an already constructed object.
418
     * @return array Returns an array suitable to be applied to a function call.
419
     */
420 17
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
421
        $args = array_change_key_case($args);
422
423 17
        $pos = 0;
424
        foreach ($defaultArgs as $name => &$arg) {
425
            if (array_key_exists($name, $args)) {
426
                // This is a named arg and should be used.
427 2
                $value = $args[$name];
428 13
            } elseif (isset($args[$pos]) && (!($arg instanceof DefaultReference) || is_a($args[$pos], $arg->getName()))) {
429
                // There is an arg at this position and it's the same type as the default arg or the default arg is typeless.
430 3
                $value = $args[$pos];
431 3
                $pos++;
432
            } else {
433
                // There is no passed arg, so use the default arg.
434 12
                $value = $arg;
435 5
            }
436
437 12
            if ($value instanceof ReferenceInterface) {
438
                $value = $value->resolve($this, $instance);
439
            }
440 12
            $arg = $value;
441
        }
442
443 17
        return $defaultArgs;
444 17
    }
445
446
    /**
447
     * Create an instance of a container item.
448
     *
449
     * This method either creates a new instance or returns an already created shared instance.
450
     *
451
     * @param string $nid The normalized ID of the container item.
452
     * @param array $args Additional arguments to pass to the constructor.
453
     * @return object Returns an object instance.
454
     */
455 30
    private function createInstance($nid, array $args) {
456
        $rule = $this->makeRule($nid);
457
458
        // Cache the instance or its factory for future use.
459 30
        if (empty($rule['shared'])) {
460 1
            $factory = $this->makeFactory($nid, $rule);
461
            $instance = $factory($args);
462 22
            $this->factories[$nid] = $factory;
463
        } else {
464 1
            $instance = $this->createSharedInstance($nid, $rule, $args);
465 22
        }
466 28
        return $instance;
467 30
    }
468
469
    /**
470
     * Call a callback with argument injection.
471
     *
472
     * @param callable $callback The callback to call.
473
     * @param array $args Additional arguments to pass to the callback.
474
     * @return mixed Returns the result of the callback.
475
     * @throws ContainerException Throws an exception if the callback cannot be understood.
476
     */
477 1
    public function call(callable $callback, array $args = []) {
478 1
        $instance = null;
479
        if (is_string($callback) || $callback instanceof \Closure) {
480
            $function = new \ReflectionFunction($callback);
481
        } elseif (is_array($callback)) {
482
            $function = new \ReflectionMethod($callback[0], $callback[1]);
483
484
            if (is_object($callback[0])) {
485 1
                $instance = $callback[0];
486
            }
487
        } else {
488
            throw new ContainerException("Could not understand callback.", 500);
489 1
        }
490
491
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
492
493
        return call_user_func_array($callback, $args);
494 1
    }
495
496
    /**
497
     * Returns true if the container can return an entry for the given identifier. Returns false otherwise.
498
     *
499
     * @param string $id Identifier of the entry to look for.
500
     *
501
     * @return boolean
502
     */
503
    public function has($id) {
504
        $id = $this->normalizeID($id);
505
506
        return isset($this->instances[$id]) || isset($this->rules[$id]) || class_exists($id);
507
    }
508
509
    /**
510
     * Finds an entry of the container by its identifier and returns it.
511
     *
512
     * @param string $id Identifier of the entry to look for.
513
     *
514
     * @throws NotFoundException  No entry was found for this identifier.
515
     * @throws ContainerException Error while retrieving the entry.
516
     *
517
     * @return mixed Entry.
518
     */
519 2
    public function get($id) {
520 2
        return $this->getArgs($id);
521
    }
522
523
    /**
524
     * Determine the reflection information for a callback.
525
     *
526
     * @param callable $callback The callback to reflect.
527
     * @return \ReflectionFunctionAbstract Returns the reflection function for the callback.
528
     */
529 9
    private function reflectCallback(callable $callback) {
530
        if (is_array($callback)) {
531
            return new \ReflectionMethod($callback[0], $callback[1]);
532
        } else {
533
            return new \ReflectionFunction($callback);
534
        }
535 9
    }
536
}
537