Container::getInjector()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 1
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
ccs 5
cts 5
cp 1
crap 3
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Core;
10
11
use Interop\Container\ContainerInterface;
12
use ReflectionFunctionAbstract as ContextFunction;
13
use Spiral\Core\Container\Autowire;
14
use Spiral\Core\Container\InjectableInterface;
15
use Spiral\Core\Container\InjectorInterface;
16
use Spiral\Core\Container\SingletonInterface;
17
use Spiral\Core\Exceptions\Container\ArgumentException;
18
use Spiral\Core\Exceptions\Container\AutowireException;
19
use Spiral\Core\Exceptions\Container\ContainerException;
20
use Spiral\Core\Exceptions\Container\InjectionException;
21
use Spiral\Core\Exceptions\Container\NotFoundException;
22
use Spiral\Core\Exceptions\LogicException;
23
24
/**
25
 * Auto-wiring container: declarative singletons, contextual injections, outer delegation and
26
 * ability to lazy wire.
27
 *
28
 * Container does not support setter injections, private properties and etc. Normally it will work
29
 * with classes only to be as much invisible as possible. Attention, this is hungry implementation
30
 * of container, meaning it WILL try to resolve dependency unless you specified custom lazy factory.
31
 *
32
 * You can use injectors to delegate class resolution to external container.
33
 *
34
 * @see \Spiral\Core\Container::registerInstance() to add your own behaviours.
35
 *
36
 * @see InjectableInterface
37
 * @see SingletonInterface
38
 */
39
class Container extends Component implements ContainerInterface, FactoryInterface, ResolverInterface
40
{
41
    /**
42
     * Outer container responsible for low level dependency configuration (i.e. config based).
43
     *
44
     * @var ContainerInterface
45
     */
46
    private $outerContainer = null;
47
48
    /**
49
     * IoC bindings.
50
     *
51
     * @what-if private
52
     * @invisible
53
     *
54
     * @var array
55
     */
56
    protected $bindings = [
57
        ContainerInterface::class => self::class,
58
        FactoryInterface::class   => self::class,
59
        ResolverInterface::class  => self::class
60
    ];
61
62
    /**
63
     * List of classes responsible for handling specific instance or interface. Provides ability to
64
     * delegate container functionality.
65
     *
66
     * @what-if private
67
     * @invisible
68
     *
69
     * @var array
70
     */
71
    protected $injectors = [];
72
73
    /**
74
     * Provide outer container in order to proxy get and has requests.
75
     *
76
     * @param ContainerInterface|null $outerContainer
77
     */
78 61
    public function __construct(ContainerInterface $outerContainer = null)
79
    {
80 61
        $this->outerContainer = $outerContainer;
81 61
        $this->bindings[static::class] = self::class;
82 61
        $this->bindings[self::class] = $this;
83 61
    }
84
85
    /**
86
     * Container can not be cloned.
87
     */
88 1
    public function __clone()
89
    {
90 1
        throw new LogicException("Container is not clonable");
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 6
    public function has($alias)
97
    {
98 6
        if ($this->outerContainer !== null && $this->outerContainer->has($alias)) {
99 1
            return true;
100
        }
101
102 5
        return array_key_exists($alias, $this->bindings);
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     *
108
     * Context parameter will be passed to class injectors, which makes possible to use this method
109
     * as:
110
     * $this->container->get(DatabaseInterface::class, 'default');
111
     *
112
     * Attention, context ignored when outer container has instance by alias.
113
     *
114
     * @param string|null $context Call context.
115
     *
116
     * @throws ContainerException
117
     */
118 35
    public function get($alias, string $context = null)
119
    {
120 35
        if ($this->outerContainer !== null && $this->outerContainer->has($alias)) {
121 1
            return $this->outerContainer->get($alias);
122
        }
123
124
        if ($alias instanceof Autowire) {
125 34
            return $alias->resolve($this);
126
        }
127
128
        //Direct bypass to construct, i might think about this option... or not.
129
        return $this->make($alias, [], $context);
130
    }
131
132
    /**
133 49
     * {@inheritdoc}
134
     *
135 49
     * @param string|null $context Related to parameter caused injection if any.
136
     */
137 39
    final public function make(string $alias, $parameters = [], string $context = null)
138
    {
139
        if (!isset($this->bindings[$alias])) {
140 31
            //No direct instructions how to construct class, make is automatically
141
            return $this->autowire($alias, $parameters, $context);
142 16
        }
143
144
        if (is_object($binding = $this->bindings[$alias])) {
145 21
            //When binding is instance, assuming singleton
146
            return $binding;
147 11
        }
148
149
        if (is_string($binding)) {
150 11
            //Binding is pointing to something else
151
            return $this->make($binding, $parameters, $context);
152 1
        }
153 11
154 7
        $instance = $this->evaluateBinding($alias, $binding[0], $parameters, $context);
155
156
        if ($binding[1]) {
157 7
            //Indicates singleton
158 7
            $this->bindings[$alias] = $instance;
159
        }
160 5
161
        return $instance;
162 4
    }
163
164
    /**
165 4
     * {@inheritdoc}
166 4
     *
167 4
     * @param string $context
168
     */
169
    final public function resolveArguments(
170 4
        ContextFunction $reflection,
171
        array $parameters = [],
172 4
        string $context = null
173
    ): array {
174
        $arguments = [];
175
        foreach ($reflection->getParameters() as $parameter) {
176 1
            try {
177
                //Information we need to know about argument in order to resolve it's value
178
                $name = $parameter->getName();
179 10
                $class = $parameter->getClass();
180
            } catch (\Throwable $e) {
181 5
                //Possibly invalid class definition or syntax error
182
                throw new ContainerException($e->getMessage(), $e->getCode(), $e);
183
            }
184 10
185
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
186 2
                if ($parameters[$name] instanceof Autowire) {
187
                    //Supplied by user as late dependency
188
                    $arguments[] = $parameters[$name]->resolve($this);
189 8
                } else {
190
                    //Supplied by user as object
191
                    $arguments[] = $parameters[$name];
192
                }
193
                continue;
194
            }
195
196
            //No declared type or scalar type or array
197 29
            if (empty($class)) {
198
                //Provided from outside
199
                if (array_key_exists($name, $parameters)) {
200
                    //Make sure it's properly typed
201
                    $this->assertType($parameter, $reflection, $parameters[$name]);
202 29
                    $arguments[] = $parameters[$name];
203 29
                    continue;
204
                }
205
206 24
                if ($parameter->isDefaultValueAvailable()) {
207 24
                    //Default value
208 1
                    $arguments[] = $parameter->getDefaultValue();
209
                    continue;
210 1
                }
211
212
                //Unable to resolve scalar argument value
213 23
                throw new ArgumentException($parameter, $reflection);
214 1
            }
215
216 1
            try {
217
                //Requesting for contextual dependency
218
                $arguments[] = $this->get($class->getName(), $name);
219
220
                continue;
221 1
            } catch (AutowireException $e) {
222
                if ($parameter->isOptional()) {
223
                    //This is optional dependency, skip
224
                    $arguments[] = null;
225 23
                    continue;
226
                }
227 21
228
                throw $e;
229 17
            }
230 16
        }
231 16
232
        return $arguments;
233
    }
234 9
235
    /**
236 8
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
237 8
     * for each method call), function array or Closure (executed every call). Only object resolvers
238
     * supported by this method.
239
     *
240
     * @param string                $alias
241 1
     * @param string|array|callable $resolver
242
     *
243
     * @return self
244
     */
245
    final public function bind(string $alias, $resolver): Container
246 7
    {
247
        if (is_array($resolver) || $resolver instanceof \Closure || $resolver instanceof Autowire) {
248 5
            //Array means = execute me, false = not singleton
249 2
            $this->bindings[$alias] = [$resolver, false];
250 2
251
            return $this;
252 1
        }
253 1
254
        $this->bindings[$alias] = $resolver;
255
256 1
        return $this;
257
    }
258
259
    /**
260 21
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
261
     * (will be constructed only once), function array or Closure (executed only once call).
262
     *
263
     * @param string                $alias
264
     * @param string|array|callable $resolver
265
     *
266
     * @return self
267
     */
268
    final public function bindSingleton(string $alias, $resolver): Container
269
    {
270
        if (is_object($resolver) && !$resolver instanceof \Closure && !$resolver instanceof Autowire) {
271
            //Direct binding to an instance
272
            $this->bindings[$alias] = $resolver;
273 25
274
            return $this;
275 25
        }
276
277 7
        $this->bindings[$alias] = [$resolver, true];
278
279 7
        return $this;
280
    }
281
282 19
    /**
283
     * Specify binding which has to be used for class injection.
284 19
     *
285
     * @param string        $class
286
     * @param string|object $injector
287
     *
288
     * @return self
289
     */
290
    public function bindInjector(string $class, $injector): Container
291
    {
292
        if (!is_string($injector)) {
293
            throw new \InvalidArgumentException('Injector can only be set as string binding');
294
        }
295
296 9
        $this->injectors[$class] = $injector;
297
298 9
        return $this;
299
    }
300 4
301
    /**
302 4
     * Check if given class has associated injector.
303
     *
304
     * @param \ReflectionClass $reflection
305 5
     *
306
     * @return bool
307 5
     */
308
    public function hasInjector(\ReflectionClass $reflection): bool
309
    {
310
        if (isset($this->injectors[$reflection->getName()])) {
311
            return true;
312
        }
313
314
        //Auto injection!
315
        return $reflection->isSubclassOf(InjectableInterface::class);
316
    }
317
318 5
    /**
319
     * Check if alias points to constructed instance (singleton).
320 5
     *
321 1
     * @param string $alias
322
     *
323
     * @return bool
324 4
     */
325
    final public function hasInstance(string $alias): bool
326 4
    {
327
        if (!$this->has($alias)) {
328
            return false;
329
        }
330
331
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
332
            //Checking alias tree
333
            $alias = $this->bindings[$alias];
334
        }
335
336 25
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
337
    }
338 25
339 3
    /**
340
     * @param string $alias
341
     */
342
    final public function removeBinding(string $alias)
343 23
    {
344
        unset($this->bindings[$alias]);
345
    }
346
347
    /**
348
     * @param string $class
349
     */
350
    final public function removeInjector(string $class)
351
    {
352
        unset($this->injectors[$class]);
353 2
    }
354
355 2
    /**
356 2
     * Every declared Container binding. Must not be used in production code due container format is
357
     * vary.
358
     *
359 2
     * @return array
360
     */
361 1
    final public function getBindings(): array
362
    {
363
        return $this->bindings;
364 2
    }
365
366
    /**
367
     * Every binded injector.
368
     *
369
     * @return array
370 1
     */
371
    final public function getInjectors(): array
372 1
    {
373 1
        return $this->injectors;
374
    }
375
376
    /**
377
     * Automatically create class.
378 1
     *
379
     * @param string $class
380 1
     * @param array  $parameters
381 1
     * @param string $context
382
     *
383
     * @return object
384
     *
385
     * @throws AutowireException
386
     * @throws \ErrorException
387
     */
388
    final protected function autowire(string $class, array $parameters, string $context = null)
389 1
    {
390
        if (!class_exists($class)) {
391 1
            throw new NotFoundException("Undefined class or binding '{$class}'");
392
        }
393
394
        //Automatically create instance
395
        $instance = $this->createInstance($class, $parameters, $context);
396
397
        //Apply registration functions to created instance
398
        return $this->registerInstance($instance, $parameters);
399 1
    }
400
401 1
    /**
402
     * Register instance in container, might perform methods like auto-singletons, log populations
403
     * and etc. Can be extended.
404
     *
405
     * @param object $instance   Created object.
406
     * @param array  $parameters Parameters which been passed with created instance.
407
     *
408
     * @return object
409
     */
410
    protected function registerInstance($instance, array $parameters)
411
    {
412
        //Declarative singletons (only when class received via direct get)
413
        if (empty($parameters) && $instance instanceof SingletonInterface) {
414
            $alias = get_class($instance);
415 39
416
            if (!isset($this->bindings[$alias])) {
417
                $this->bindings[$alias] = $instance;
418 39
            }
419 38
        }
420
421 6
        //Your code can go here (for example LoggerAwareInterface, custom hydration and etc)
422
423 1
        return $instance;
424
    }
425
426
    /**
427 38
     * @param string      $alias
428
     * @param mixed       $target Value binded by user.
429
     * @param array       $parameters
430 25
     * @param string|null $context
431
     *
432
     * @return mixed|null|object
433
     *
434
     * @throws ContainerException
435
     * @throws \Interop\Container\Exception\ContainerException
436
     */
437
    private function evaluateBinding(
438
        string $alias,
439
        $target,
440
        array $parameters,
441
        string $context = null
442 25
    ) {
443
        if (is_string($target)) {
444
            //Reference
445 25
            return $this->make($target, $parameters, $context);
446 1
        }
447
448 1
        if ($target instanceof Autowire) {
449 1
            return $target->resolve($this, $parameters);
450
        }
451
452
        if ($target instanceof \Closure) {
453
            $reflection = new \ReflectionFunction($target);
454
455 25
            //Invoking Closure with resolved arguments
456
            return $reflection->invokeArgs(
457
                $this->resolveArguments($reflection, $parameters, $context)
458
            );
459
        }
460
461
        if (is_array($target) && isset($target[1])) {
462
            //In a form of resolver and method
463
            list($resolver, $method) = $target;
464
465
            //Resolver instance (i.e. [ClassName::class, 'method'])
466
            $resolver = $this->get($resolver);
467
            $method = new \ReflectionMethod($resolver, $method);
468
            $method->setAccessible(true);
469 38
470
            //Invoking factory method with resolved arguments
471 38
            return $method->invokeArgs(
472
                $resolver,
473
                $this->resolveArguments($method, $parameters, $context)
474 38
            );
475 11
        }
476
477 6
        throw new ContainerException("Invalid binding for '{$alias}'");
478 1
    }
479
480
    /**
481 5
     * Create instance of desired class.
482
     *
483
     * @param string      $class
484 28
     * @param array       $parameters Constructor parameters.
485 1
     * @param string|null $context
486
     *
487
     * @return object
488 27
     *
489
     * @throws ContainerException
490 21
     * @throws \ErrorException
491 21
     */
492
    private function createInstance(string $class, array $parameters, string $context = null)
493
    {
494
        $reflection = new \ReflectionClass($class);
495 8
496
        //We have to construct class using external injector when we know exact context
497
        if (empty($parameters) && $this->hasInjector($reflection)) {
498 20
            $instance = $this->getInjector($reflection)->createInjection($reflection, $context);
499
500
            if (!$reflection->isInstance($instance)) {
501
                throw new InjectionException("Invalid injection response for '{$reflection->getName()}'");
502
            }
503
504
            return $instance;
505
        }
506
507
        if (!$reflection->isInstantiable()) {
508 11
            throw new ContainerException("Class '{$class}' can not be constructed");
509
        }
510 11
511
        if (!empty($constructor = $reflection->getConstructor())) {
512 3
            //Using constructor with resolved arguments
513
            $instance = $reflection->newInstanceArgs(
514
                $this->resolveArguments($constructor, $parameters)
515 8
            );
516
        } else {
517
            //No constructor specified
518 8
            $instance = $reflection->newInstance();
519 2
        }
520 2
521
        return $instance;
522
    }
523
524 6
    /**
525
     * Get injector associated with given class.
526
     *
527
     * @param \ReflectionClass $reflection
528
     *
529
     * @return InjectorInterface
530
     */
531
    private function getInjector(\ReflectionClass $reflection): InjectorInterface
532
    {
533
        if (isset($this->injectors[$reflection->getName()])) {
534
            //Stated directly
535
            $injector = $this->get($this->injectors[$reflection->getName()]);
536 17
        } else {
537
            //Auto-injection!
538
            $injector = $this->get($reflection->getConstant('INJECTOR'));
539
        }
540
541 17
        if (!$injector instanceof InjectorInterface) {
542 2
            throw new InjectionException(
543 1
                "Class '" . get_class($injector) . "' must be an instance of InjectorInterface for '{$reflection->getName()}'"
544
            );
545
        }
546 1
547
        return $injector;
548
    }
549 16
550
    /**
551 16
     * Assert that given value are matched parameter type.
552 1
     *
553
     * @param \ReflectionParameter        $parameter
554
     * @param \ReflectionFunctionAbstract $context
555 16
     * @param mixed                       $value
556 2
     *
557
     * @throws ArgumentException
558
     */
559 16
    private function assertType(
560 1
        \ReflectionParameter $parameter,
561
        \ReflectionFunctionAbstract $context,
562 16
        $value
563
    ) {
564
        if (is_null($value)) {
565
            if (
566
                !$parameter->isOptional()
567
                && !($parameter->isDefaultValueAvailable() && $parameter->getDefaultValue() === null)
568
            ) {
569
                throw new ArgumentException($parameter, $context);
570
            }
571
572
            return;
573
        }
574
575
        $type = $parameter->getType();
576
        $typeName = method_exists($type, 'getName') ? $type->getName() : (string)$type;
577
578
        if ($typeName == 'array' && !is_array($value)) {
579
            throw new ArgumentException($parameter, $context);
580
        }
581
582
        if (($typeName == 'int' || $typeName == 'float') && !is_numeric($value)) {
583
            throw new ArgumentException($parameter, $context);
584
        }
585
586
        if ($typeName == 'bool' && !is_bool($value) && !is_numeric($value)) {
587
            throw new ArgumentException($parameter, $context);
588
        }
589
    }
590
}
591