Completed
Push — master ( 17598f...abee1f )
by Kirill
13s queued 11s
created

Container::checkInjector()   B

Complexity

Conditions 10
Paths 6

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 22
nc 6
nop 1
dl 0
loc 39
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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