Completed
Push — master ( 1d3938...6e15d2 )
by Todd
9s
created

Container::setShared()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 3
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' => []]];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
26 36
        $this->currentRule = &$this->rules['*'];
27 36
        $this->instances = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
28 36
        $this->factories = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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 35
    private function normalizeID($id) {
38 35
        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 9
    public function rule($id) {
58 9
        $id = $this->normalizeID($id);
59
60 9
        if (!isset($this->rules[$id])) {
61 9
            $this->rules[$id] = [];
62 9
        }
63 9
        $this->currentRule = &$this->rules[$id];
64 9
        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
    }
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 12
    public function setShared($value) {
96 12
        $this->currentRule['shared'] = $value;
97 12
        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 8
    public function setConstructorArgs(array $args) {
118 8
        $this->currentRule['constructorArgs'] = $args;
119 8
        return $this;
120
    }
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 5
    public function setInstance($name, $instance) {
133 5
        $this->instances[$this->normalizeID($name)] = $instance;
134 5
        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 6
    public function addCall($method, array $args = []) {
145 6
        $this->currentRule['calls'][] = [$method, $args];
146
147 6
        return $this;
148
    }
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 35
    public function getArgs($id, array $args = []) {
162 35
        $id = $this->normalizeID($id);
163
164 35
        if (isset($this->instances[$id])) {
165
            // A shared instance just gets returned.
166 8
            return $this->instances[$id];
167
        }
168
169 31
        if (isset($this->factories[$id])) {
170
            // The factory for this object type is already there so call it to create the instance.
171 2
            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 31
        return $this->createInstance($id, $args);
177
    }
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 31
    private function makeRule($nid) {
186 31
        $rule = isset($this->rules[$nid]) ? $this->rules[$nid] : [];
187
188 31
        if (class_exists($nid)) {
189 23
            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 1
            }
196
197
            // Add the default rule.
198 23
            if (!empty($this->rules['*']['inherit'])) {
199 23
                $rule += $this->rules['*'];
200 23
            }
201
202
            // Add interface calls to the rule.
203 23
            $interfaces = class_implements($nid);
204 23
            foreach ($interfaces as $interface) {
205 6
                if (!empty($this->rules[$interface]['calls'])
206 6
                    && (!isset($this->rules[$interface]['inherit']) || $this->rules[$interface]['inherit'] !== false)
207 6
                ) {
208
209 2
                    $rule['calls'] = array_merge(
210 2
                        isset($rule['calls']) ? $rule['calls'] : [],
211 2
                        $this->rules[$interface]['calls']
212 2
                    );
213 2
                }
214 23
            }
215 31
        } elseif (!empty($this->rules['*']['inherit'])) {
216
            // Add the default rule.
217 9
            $rule += $this->rules['*'];
218 9
        }
219
220 31
        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 25
    private function makeFactory($nid, array $rule) {
232 25
        $className = empty($rule['class']) ? $nid : $rule['class'];
233
234 25
        if (!empty($rule['factory'])) {
235
            // The instance is created with a user-supplied factory function.
236 7
            $callback = $rule['factory'];
237 7
            $function = $this->reflectCallback($callback);
238
239 7
            if ($function->getNumberOfParameters() > 0) {
240 3
                $callbackArgs = $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule);
241
                $factory = function ($args) use ($callback, $callbackArgs) {
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
242 3
                    return call_user_func_array($callback, $this->resolveArgs($callbackArgs, $args));
243 3
                };
244 3
            } else {
245 4
                $factory = $callback;
246
            }
247
248
            // If a class is specified then still reflect on it so that calls can be made against it.
249 7
            if (class_exists($className)) {
250 2
                $class = new \ReflectionClass($className);
251 2
            }
252 7
        } else {
253
            // The instance is created by newing up a class.
254 18
            if (!class_exists($className)) {
255 1
                throw new NotFoundException("Class $className does not exist.", 404);
256
            }
257 17
            $class = new \ReflectionClass($className);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
258 17
            $constructor = $class->getConstructor();
259
260 17
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
261 14
                $constructorArgs = $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule);
262
263
                $factory = function ($args) use ($class, $constructorArgs) {
264 14
                    return $class->newInstanceArgs($this->resolveArgs($constructorArgs, $args));
265 14
                };
266 14
            } else {
267
                $factory = function () use ($className) {
268 3
                    return new $className;
269 3
                };
270
            }
271
        }
272
273
        // Add calls to the factory.
274 24
        if (isset($class) && !empty($rule['calls'])) {
275 5
            $calls = [];
276
277
            // Generate the calls array.
278 5
            foreach ($rule['calls'] as $call) {
279 5
                list($methodName, $args) = $call;
280 5
                $method = $class->getMethod($methodName);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 18 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
281 5
                $calls[] = [$methodName, $this->makeDefaultArgs($method, $args, $rule)];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 17 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
282 5
            }
283
284
            // Wrap the factory in one that makes the calls.
285 5
            $factory = function ($args) use ($factory, $calls) {
286 5
                $instance = $factory($args);
287
288 5
                foreach ($calls as $call) {
289 5
                    call_user_func_array(
290 5
                        [$instance, $call[0]],
291 5
                        $this->resolveArgs($call[1], [], $instance)
292 5
                    );
293 5
                }
294
295 5
                return $instance;
296 5
            };
297 5
        }
298
299 24
        return $factory;
300
    }
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 7
    private function createSharedInstance($nid, array $rule, array $args) {
314 7
        $className = empty($rule['class']) ? $nid : $rule['class'];
315
316 7
        if (!empty($rule['factory'])) {
317
            // The instance is created with a user-supplied factory function.
318 2
            $callback = $rule['factory'];
319 2
            $function = $this->reflectCallback($callback);
320
321 2
            if ($function->getNumberOfParameters() > 0) {
322 1
                $callbackArgs = $this->resolveArgs(
323 1
                    $this->makeDefaultArgs($function, $rule['constructorArgs'], $rule),
324
                    $args
325 1
                );
326
327 1
                $this->instances[$nid] = null; // prevent cyclic dependency from infinite loop.
328 1
                $this->instances[$nid] = $instance = call_user_func_array($callback, $callbackArgs);
329 1
            } else {
330 1
                $this->instances[$nid] = $instance = $callback();
331
            }
332
333
            // If a class is specified then still reflect on it so that calls can be made against it.
334 2
            if (class_exists($className)) {
335
                $class = new \ReflectionClass($className);
336
            }
337 2
        } else {
338 5
            if (!class_exists($className)) {
339 1
                throw new NotFoundException("Class $className does not exist.", 404);
340
            }
341 4
            $class = new \ReflectionClass($className);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
342 4
            $constructor = $class->getConstructor();
343
344 4
            if ($constructor && $constructor->getNumberOfParameters() > 0) {
345 3
                $constructorArgs = $this->resolveArgs(
346 3
                    $this->makeDefaultArgs($constructor, $rule['constructorArgs'], $rule),
347
                    $args
348 3
                );
349
350
                // Instantiate the object first so that this instance can be used for cyclic dependencies.
351 3
                $this->instances[$nid] = $instance = $class->newInstanceWithoutConstructor();
352 3
                $constructor->invokeArgs($instance, $constructorArgs);
353 3
            } else {
354 1
                $this->instances[$nid] = $instance = new $class->name;
355
            }
356
        }
357
358
        // Call subsequent calls on the new object.
359 6
        if (isset($class) && !empty($rule['calls'])) {
360 1
            foreach ($rule['calls'] as $call) {
361 1
                list($methodName, $args) = $call;
362 1
                $method = $class->getMethod($methodName);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 18 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
363
364 1
                $args = $this->resolveArgs(
365 1
                    $this->makeDefaultArgs($method, $args, $rule),
366 1
                    [],
367
                    $instance
368 1
                );
369
370 1
                $method->invokeArgs($instance, $args);
371 1
            }
372 1
        }
373
374 6
        return $instance;
375
    }
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 24
    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 24
        $ruleArgs = array_change_key_case($ruleArgs);
387 24
        $result = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
388
389 24
        $pos = 0;
390 24
        foreach ($function->getParameters() as $i => $param) {
391 24
            $name = strtolower($param->name);
392
393 24
            if (array_key_exists($name, $ruleArgs)) {
394 1
                $value = $ruleArgs[$name];
395 24
            } elseif ($param->getClass()) {
396 5
                $value = new DefaultReference($this->normalizeID($param->getClass()->getName()));
397 23
            } elseif (array_key_exists($pos, $ruleArgs)) {
398 10
                $value = $ruleArgs[$pos];
399 10
                $pos++;
400 23
            } elseif ($param->isDefaultValueAvailable()) {
401 12
                $value = $param->getDefaultValue();
402 12
            } else {
403 1
                $value = null;
404
            }
405
406 24
            $result[$name] = $value;
407 24
        }
408
409 24
        return $result;
410
    }
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 24
    private function resolveArgs(array $defaultArgs, array $args, $instance = null) {
421 24
        $args = array_change_key_case($args);
422
423 24
        $pos = 0;
424 24
        foreach ($defaultArgs as $name => &$arg) {
425 24
            if (array_key_exists($name, $args)) {
426
                // This is a named arg and should be used.
427 2
                $value = $args[$name];
428 24
            } 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 4
                $value = $args[$pos];
431 4
                $pos++;
432 4
            } else {
433
                // There is no passed arg, so use the default arg.
434 21
                $value = $arg;
435
            }
436
437 24
            if ($value instanceof ReferenceInterface) {
438 5
                $value = $value->resolve($this, $instance);
439 5
            }
440 24
            $arg = $value;
441 24
        }
442
443 24
        return $defaultArgs;
444
    }
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 31
    private function createInstance($nid, array $args) {
456 31
        $rule = $this->makeRule($nid);
457
458
        // Cache the instance or its factory for future use.
459 31
        if (empty($rule['shared'])) {
460 25
            $factory = $this->makeFactory($nid, $rule);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 15 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
461 24
            $instance = $factory($args);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 14 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
462 24
            $this->factories[$nid] = $factory;
463 24
        } else {
464 7
            $instance = $this->createSharedInstance($nid, $rule, $args);
465
        }
466 29
        return $instance;
467
    }
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 1
        if (is_string($callback) || $callback instanceof \Closure) {
480
            $function = new \ReflectionFunction($callback);
481 1
        } elseif (is_array($callback)) {
482 1
            $function = new \ReflectionMethod($callback[0], $callback[1]);
483
484 1
            if (is_object($callback[0])) {
485 1
                $instance = $callback[0];
486 1
            }
487 1
        } else {
488
            throw new ContainerException("Could not understand callback.", 500);
489
        }
490
491 1
        $args = $this->resolveArgs($this->makeDefaultArgs($function, $args), [], $instance);
492
493 1
        return call_user_func_array($callback, $args);
494
    }
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 30
    public function get($id) {
520 30
        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 9
        if (is_array($callback)) {
531 2
            return new \ReflectionMethod($callback[0], $callback[1]);
532
        } else {
533 7
            return new \ReflectionFunction($callback);
534
        }
535
    }
536
}
537