Completed
Push — master ( 27937e...bdb326 )
by Rasmus
22:58 queued 22:58
created

Container::ref()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace mindplay\unbox;
4
5
use Closure;
6
use Interop\Container\ContainerInterface;
7
use InvalidArgumentException;
8
use ReflectionClass;
9
use ReflectionFunction;
10
use ReflectionFunctionAbstract;
11
use ReflectionMethod;
12
use ReflectionParameter;
13
14
/**
15
 * This class implements a simple dependency injection container.
16
 */
17
class Container implements ContainerInterface, FactoryInterface
18
{
19
    /**
20
     * @type string pattern for parsing an argument type from a ReflectionParameter string
21
     * @see getArgumentType()
22
     */
23
    const ARG_PATTERN = '/(?:\<required\>|\<optional\>)\\s+([\\w\\\\]+)/';
24
25
    /**
26
     * @var mixed[] map where component name => value
27
     */
28
    protected $values = array();
29
30
    /**
31
     * @var callable[] map where component name => factory function
32
     */
33
    protected $factory = array();
34
35
    /**
36
     * @var array map where component name => mixed list/map of parameter names
37
     */
38
    protected $factory_map = array();
39
40
    /**
41
     * @var bool[] map where component name => true (if the component is immutable)
42
     */
43
    protected $immutable = array();
44
45
    /**
46
     * @var (callable[])[] map where component name => list of configuration functions
47
     */
48
    protected $config = array();
49
50
    /**
51
     * @var array map where component name => mixed list/map of parameter names
52
     */
53
    protected $config_map = array();
54
55
    /**
56
     * Self-register this container for dependency injection
57
     */
58 1
    public function __construct()
59
    {
60 1
        $this->values[get_class($this)] =
61 1
        $this->values[__CLASS__] =
62 1
        $this->values[ContainerInterface::class] =
63 1
        $this->values[FactoryInterface::class] =
64
            $this;
65 1
    }
66
67
    /**
68
     * Resolve the registered component with the given name.
69
     *
70
     * @param string $name component name
71
     *
72
     * @return mixed
73
     *
74
     * @throws ContainerException
75
     * @throws NotFoundException
76
     */
77 1
    public function get($name)
78
    {
79 1
        if (!array_key_exists($name, $this->values)) {
80 1
            if (!isset($this->factory[$name])) {
81 1
                throw new NotFoundException($name);
82
            }
83
84 1
            $factory = $this->factory[$name];
85
86 1
            $reflection = new ReflectionFunction($factory);
87
88 1
            $params = $this->resolve($reflection->getParameters(), $this->factory_map[$name]);
89
90 1
            $this->values[$name] = call_user_func_array($factory, $params);
91
92 1
            $this->initialize($name);
93
94 1
            $this->immutable[$name] = true; // prevent further changes to this component
95 1
        }
96
97 1
        return $this->values[$name];
98
    }
99
100
    /**
101
     * Directly inject a component into the container - use this to register components that
102
     * have already been created for some reason; for example, the Composer ClassLoader.
103
     *
104
     * @param string $name component name
105
     * @param mixed  $value
106
     *
107
     * @return void
108
     *
109
     * @throws ContainerException
110
     */
111 1
    public function set($name, $value)
112
    {
113 1
        if (isset($this->immutable[$name])) {
114 1
            throw new ContainerException("attempted overwrite of initialized component: {$name}");
115
        }
116
117 1
        $this->values[$name] = $value;
118
119 1
        $this->initialize($name);
120 1
    }
121
122
    /**
123
     * Register a component for dependency injection.
124
     *
125
     * There are numerous valid ways to register components.
126
     *
127
     *   * `register(Foo::class)` registers a component by it's class-name, and will try to
128
     *     automatically resolve all of it's constructor arguments.
129
     *
130
     *   * `register(Foo::class, ['bar'])` registers a component by it's class-name, and will
131
     *     use `'bar'` as the first constructor argument, and try to resolve the rest.
132
     *
133
     *   * `register(Foo::class, [$container->ref(Bar::class)])` creates a boxed reference to
134
     *     a registered component `Bar` and provides that as the first argument.
135
     *
136
     *   * `register(Foo::class, ['bat' => 'zap'])` registers a component by it's class-name
137
     *     and will use `'zap'` for the constructor argument named `$bat`, and try to resolve
138
     *     any other arguments.
139
     *
140
     *   * `register(Bar::class, Foo::class)` registers a component `Foo` under another name
141
     *     `Bar`, which might be an interface or an abstract class.
142
     *
143
     *   * `register(Bar::class, Foo::class, ['bar'])` same as above, but uses `'bar'` as the
144
     *     first argument.
145
     *
146
     *   * `register(Bar::class, Foo::class, ['bat' => 'zap'])` same as above, but, well, guess.
147
     *
148
     *   * `register(Bar::class, function (Foo $foo) { return new Bar(...); })` registers a
149
     *     component with a custom creation function.
150
     *
151
     *   * `register(Bar::class, function ($name) { ... }, [$container->ref('db.name')]);`
152
     *     registers a component creation function with a reference to a component "db.name"
153
     *     as the first argument.
154
     *
155
     * In effect, you can think of `$func` as being an optional argument.
156
     *
157
     * The provided parameter values may include any `BoxedValueInterface`, such as the boxed
158
     * component referenced created by {@see Container::ref()} - these will be unboxed as late
159
     * as possible.
160
     *
161
     * @param string                      $name                component name
162
     * @param callable|mixed|mixed[]|null $func_or_map_or_type creation function or class-name, or, if the first
163
     *                                                         argument is a class-name, a map of constructor arguments
164
     * @param mixed|mixed[]               $map                 mixed list/map of parameter values (and/or boxed values)
165
     *
166
     * @return void
167
     *
168
     * @throws ContainerException
169
     */
170 1
    public function register($name, $func_or_map_or_type = null, $map = array())
171
    {
172 1
        if (@$this->immutable[$name]) {
173 1
            throw new ContainerException("attempted re-registration of active component: {$name}");
174
        }
175
176 1
        if (is_callable($func_or_map_or_type)) {
177
            // second argument is a creation function
178 1
            $func = $func_or_map_or_type;
179 1
        } elseif (is_string($func_or_map_or_type)) {
180
            // second argument is a class-name
181
            $func = function () use ($func_or_map_or_type, $map) {
182 1
                return $this->create($func_or_map_or_type, $map);
183 1
            };
184 1
        } elseif (is_array($func_or_map_or_type)) {
185
            // second argument is a map of constructor arguments
186
            $func = function () use ($name, $func_or_map_or_type) {
187 1
                return $this->create($name, $func_or_map_or_type);
188 1
            };
189 1
        } elseif (is_null($func_or_map_or_type)) {
190
            // first argument is both the component and class-name
191
            $func = function () use ($name) {
192 1
                return $this->create($name);
193 1
            };
194 1
        } else {
195 1
            throw new InvalidArgumentException("unexpected argument type for \$func_or_map_or_type: " . gettype($func_or_map_or_type));
196
        }
197
198 1
        $this->factory[$name] = $func;
199
200 1
        $this->factory_map[$name] = $map;
201
202 1
        unset($this->values[$name]);
203 1
    }
204
205
    /**
206
     * Register a component as an alias of another registered component.
207
     *
208
     * @param string $name     new component name
209
     * @param string $ref_name existing component name
210
     */
211
    public function alias($name, $ref_name)
212
    {
213 1
        $this->register($name, function () use ($ref_name) {
214 1
            return $this->get($ref_name);
215 1
        });
216 1
    }
217
218
    /**
219
     * Register a configuration function, which will be applied as late as possible, e.g.
220
     * on first use of the component. For example:
221
     *
222
     *     $container->configure('stack', function (MiddlewareStack $stack) {
223
     *         $stack->push(new MoreAwesomeMiddleware());
224
     *     });
225
     *
226
     * The given configuration function should include the configured component as the
227
     * first parameter to the closure, but may include any number of parameters, which
228
     * will be resolved and injected.
229
     *
230
     * The first argument (component name) is optional - that is, the name can be inferred
231
     * from the first parameter of the closure; the following will work:
232
     *
233
     *     $container->configure(function (PageLayout $layout) {
234
     *         $layout->title = "Welcome";
235
     *     });
236
     *
237
     * In some cases, such as using component names like "cache.path" (which because of the
238
     * dot in the name cannot be resolved by parameter name), you can use a boxed reference
239
     * in the optional `$map` argument, e.g.:
240
     *
241
     *     $container->configure(
242
     *         function (FileCache $cache, $path) {
243
     *             $cache->setPath($path);
244
     *         },
245
     *         ['path' => $container->ref('cache.path')]
246
     *     );
247
     *
248
     * You may optionally provide a list/map of parameter values, similar to the one
249
     * accepted by {@see Container::register()} - the typical reason to use this, is if
250
     * you need to inject another component by name, e.g. using {@see Container::ref()}.
251
     *
252
     * You can also use `configure()` to decorate objects, or manipulate (or replace) values:
253
     *
254
     *     $container->configure('num_kittens', function ($num_kittens) {
255
     *         return $num_kittens + 6; // add another litter
256
     *     });
257
     *
258
     * In other words, if your closure returns something, the component will be replaced.
259
     *
260
     * @param string|callable        $name_or_func component name
261
     *                                             (or callable, if name is left out)
262
     * @param callable|mixed|mixed[] $func_or_map  `function (Type $component, ...) : void`
263
     *                                             (or parameter values, if name is left out)
264
     * @param mixed|mixed[]          $map          mixed list/map of parameter values and/or boxed values
265
     *                                             (or unused, if name is left out)
266
     *
267
     * @return void
268
     *
269
     * @throws NotFoundException
270
     */
271 1
    public function configure($name_or_func, $func_or_map = null, $map = [])
272
    {
273 1
        if (is_callable($name_or_func)) {
274 1
            $func = $name_or_func;
275 1
            $map = $func_or_map;
276
277
            // no component name supplied, infer it from the closure:
278
279 1
            if ($func instanceof Closure) {
280 1
                $param = new ReflectionParameter($func, 0); // shortcut reflection for closures (as an optimization)
281 1
            } else {
282 1
                list($param) = $this->reflect($func)->getParameters();
283
            }
284
285
            // obtain the type-hint, but avoid triggering autoload:
286
287 1
            $name = preg_match(self::ARG_PATTERN, $param->__toString(), $matches) === 1
288 1
                ? $matches[1] // infer component name from type-hint
289 1
                : $param->name; // infer component name from parameter name
290
291 1
            if (!$this->has($name) && $this->has($param->name)) {
292
                $name = $param->name;
293
            }
294 1
        } else {
295 1
            $name = $name_or_func;
296 1
            $func = $func_or_map;
297
298 1
            if (!array_key_exists(0, $map)) {
299 1
                $map[0] = $this->ref($name);
300 1
            }
301
        }
302
303 1
        $this->config[$name][] = $func;
304 1
        $this->config_map[$name][] = $map;
305
306 1
        if ($this->isActive($name)) {
307
            // component is already active - initialize the component immediately:
308
309 1
            $this->initialize($name);
310 1
        }
311 1
    }
312
313
    /**
314
     * Check for the existence of a component with a given name.
315
     *
316
     * @param string $name component name
317
     *
318
     * @return bool true, if a component with the given name has been defined
319
     */
320 1
    public function has($name)
321
    {
322 1
        return array_key_exists($name, $this->values)
323 1
        || isset($this->factory[$name]);
324
    }
325
326
    /**
327
     * Check if a component has been unboxed and is currently active.
328
     *
329
     * @param string $name component name
330
     *
331
     * @return bool
332
     */
333 1
    public function isActive($name)
334
    {
335 1
        return array_key_exists($name, $this->values);
336
    }
337
338
    /**
339
     * Call any given callable, using dependency injection to satisfy it's arguments, and/or
340
     * manually specifying some of those arguments - then return the value from the call.
341
     *
342
     * This will work for any callable:
343
     *
344
     *     $container->call('foo');               // function foo()
345
     *     $container->call($foo, 'baz');         // instance method $foo->baz()
346
     *     $container->call([Foo::class, 'bar']); // static method Foo::bar()
347
     *     $container->call($foo);                // closure (or class implementing __invoke)
348
     *
349
     * In any of those examples, you can also supply custom arguments, either named or
350
     * positional, or mixed, as per the `$map` argument in `register()`, `configure()`, etc.
351
     *
352
     * @param callable|object $callback any arbitrary closure or callable, or object implementing __invoke()
353
     * @param mixed|mixed[]   $map      mixed list/map of parameter values (and/or boxed values)
354
     *
355
     * @return mixed return value from the given callable
356
     */
357 1
    public function call($callback, $map = array())
358
    {
359 1
        $params = $this->reflect($callback)->getParameters();
360
361 1
        return call_user_func_array($callback, $this->resolve($params, $map));
362
    }
363
364
    /**
365
     * Create an instance of a given class.
366
     *
367
     * The container will internally resolve and inject any constructor arguments
368
     * not explicitly provided in the (optional) second parameter.
369
     *
370
     * @param string        $class_name fully-qualified class-name
371
     * @param mixed|mixed[] $map        mixed list/map of parameter values (and/or boxed values)
372
     *
373
     * @return mixed
374
     */
375 1
    public function create($class_name, $map = array())
376
    {
377 1
        if (!class_exists($class_name)) {
378 1
            throw new InvalidArgumentException("unable to create component: {$class_name}");
379
        }
380
381 1
        $reflection = new ReflectionClass($class_name);
382
383 1
        if (!$reflection->isInstantiable()) {
384 1
            throw new InvalidArgumentException("unable to create instance of abstract class: {$class_name}");
385
        }
386
387 1
        $constructor = $reflection->getConstructor();
388
389
        $params = $constructor
390 1
            ? $this->resolve($constructor->getParameters(), $map)
391 1
            : array();
392
393 1
        return $reflection->newInstanceArgs($params);
394
    }
395
396
    /**
397
     * Creates a boxed reference to a component in the container.
398
     *
399
     * You can use this in conjunction with `register()` to provide a component reference
400
     * without expanding that reference until first use - for example:
401
     *
402
     *     $container->register(UserRepo::class, [$container->ref('cache')]);
403
     *
404
     * This will reference the "cache" component and provide it as the first argument to the
405
     * constructor of `UserRepo` - compared with using `$container->get('cache')`, this has
406
     * the advantage of not actually activating the "cache" component until `UserRepo` is
407
     * used for the first time.
408
     *
409
     * Another reason (besides performance) to use references, is to defer the reference:
410
     *
411
     *     $container->register(FileCache::class, ['root_path' => $container->ref('cache.path')]);
412
     *
413
     * In this example, the component "cache.path" will be fetched from the container on
414
     * first use of `FileCache`, giving you a chance to configure "cache.path" later.
415
     *
416
     * @param string $name component name
417
     *
418
     * @return BoxedValueInterface boxed component reference
419
     */
420 1
    public function ref($name)
421
    {
422 1
        return new BoxedReference($this, $name);
423
    }
424
425
    /**
426
     * Add a packaged configuration (a "provider") to this container.
427
     *
428
     * @see ProviderInterface
429
     *
430
     * @param ProviderInterface $provider
431
     *
432
     * @return void
433
     */
434 1
    public function add(ProviderInterface $provider)
435
    {
436 1
        $provider->register($this);
437 1
    }
438
439
    /**
440
     * Internally reflect on any type of callable
441
     *
442
     * @param callable $callback
443
     *
444
     * @return ReflectionFunctionAbstract
445
     */
446 1
    protected function reflect($callback)
447
    {
448 1
        if (is_object($callback)) {
449 1
            if ($callback instanceof Closure) {
450 1
                return new ReflectionFunction($callback);
451 1
            } elseif (method_exists($callback, '__invoke')) {
452 1
                return new ReflectionMethod($callback, '__invoke');
453
            }
454
455 1
            throw new InvalidArgumentException("class " . get_class($callback) . " does not implement __invoke()");
456 1
        } elseif (is_array($callback)) {
457 1
            if (is_callable($callback)) {
458 1
                return new ReflectionMethod($callback[0], $callback[1]);
459
            }
460
461 1
            throw new InvalidArgumentException("expected callable");
462 1
        } elseif (is_callable($callback)) {
463 1
            return new ReflectionFunction($callback);
464
        }
465
466 1
        throw new InvalidArgumentException("unexpected value: " . var_export($callback, true) . " - expected callable");
467
    }
468
469
    /**
470
     * Internally resolves parameters to functions or constructors.
471
     *
472
     * This is the heart of the beast.
473
     *
474
     * @param ReflectionParameter[] $params parameter reflections
475
     * @param mixed|mixed[]         $map    mixed list/map of parameter values (and/or boxed values)
476
     *
477
     * @return array parameters
478
     *
479
     * @throws ContainerException
480
     * @throws NotFoundException
481
     */
482 1
    protected function resolve(array $params, $map)
483
    {
484 1
        $args = array();
485
486 1
        $map = (array)$map;
487
488 1
        foreach ($params as $index => $param) {
489 1
            $param_name = $param->name;
490
491 1
            if (array_key_exists($param_name, $map)) {
492 1
                $value = $map[$param_name]; // // resolve as user-provided named argument
493 1
            } elseif (array_key_exists($index, $map)) {
494 1
                $value = $map[$index]; // resolve as user-provided positional argument
495 1
            } else {
496
                // as on optimization, obtain the argument type without triggering autoload:
497
498 1
                $type = preg_match(self::ARG_PATTERN, $param->__toString(), $matches)
499 1
                    ? $matches[1]
500 1
                    : null; // no type-hint available
501
502 1
                if ($type && isset($map[$type])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
503 1
                    $value = $map[$type]; // resolve as user-provided type-hinted argument
504 1
                } elseif ($type && $this->has($type)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
505 1
                    $value = $this->get($type); // resolve as component registered by class/interface name
506 1
                } elseif ($this->has($param_name)) {
507 1
                    $value = $this->get($param_name); // resolve as component with matching parameter name
508 1
                } elseif ($param->isOptional()) {
509 1
                    $value = $param->getDefaultValue(); // unresolved: resolve using default value
510 1
                } else {
511
                    // unresolved - throw a container exception:
512
513 1
                    $reflection = $param->getDeclaringFunction();
514
515 1
                    throw new ContainerException(
516 1
                        "unable to resolve \"{$type}\" for parameter: \${$param_name}" .
517 1
                        ' in file: ' . $reflection->getFileName() . ', line ' . $reflection->getStartLine()
518 1
                    );
519
                }
520
            }
521
522 1
            if ($value instanceof BoxedValueInterface) {
523 1
                $value = $value->unbox(); // unbox a boxed value
524 1
            }
525
526 1
            $args[] = $value; // argument resolved!
527 1
        }
528
529 1
        return $args;
530
    }
531
532
    /**
533
     * Internally initialize an active component.
534
     *
535
     * @param string $name component name
536
     *
537
     * @return void
538
     *
539
     * @throws ContainerException on attempt to initialize an already-initialized component
540
     */
541 1
    protected function initialize($name)
542
    {
543 1
        if (isset($this->config[$name])) {
544 1
            foreach ($this->config[$name] as $index => $config) {
545 1
                $map = $this->config_map[$name][$index];
546
547 1
                $reflection = $this->reflect($config);
548
549 1
                $params = $this->resolve($reflection->getParameters(), $map);
550
551 1
                $value = call_user_func_array($config, $params);
552
553 1
                if ($value !== null) {
554 1
                    $this->values[$name] = $value;
555 1
                }
556 1
            }
557 1
        }
558
559 1
        unset($this->config[$name]);
560 1
    }
561
}
562