Completed
Push — master ( 3bb5d0...b9b8f3 )
by Anton
02:46
created

Container   D

Complexity

Total Complexity 81

Size/Duplication

Total Lines 553
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 99.35%

Importance

Changes 0
Metric Value
dl 0
loc 553
ccs 154
cts 155
cp 0.9935
rs 4.8717
c 0
b 0
f 0
wmc 81
lcom 1
cbo 10

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A __clone() 0 4 1
A has() 0 8 3
A get() 0 13 4
B make() 0 26 5
C resolveArguments() 0 65 11
A bind() 0 13 4
A bindSingleton() 0 13 4
A bindInjector() 0 10 2
A hasInjector() 0 9 2
B hasInstance() 0 13 5
A removeBinding() 0 4 1
A removeInjector() 0 4 1
A getBindings() 0 4 1
A getInjectors() 0 4 1
A autowire() 0 17 3
A registerInstance() 0 15 4
B evaluateBinding() 0 42 6
B createInstance() 0 31 6
A getInjector() 0 18 3
C assertType() 0 30 13

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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
     */
387
    final protected function autowire(string $class, array $parameters, string $context = null)
388
    {
389 1
        try {
390
            if (!class_exists($class)) {
391 1
                throw new NotFoundException("Undefined class or binding '{$class}'");
392
            }
393
        } catch (\Error $e) {
394
            //Issues with syntax or class definition
395
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
396
        }
397
398
        //Automatically create instance
399 1
        $instance = $this->createInstance($class, $parameters, $context);
400
401 1
        //Apply registration functions to created instance
402
        return $this->registerInstance($instance, $parameters);
403
    }
404
405
    /**
406
     * Register instance in container, might perform methods like auto-singletons, log populations
407
     * and etc. Can be extended.
408
     *
409
     * @param object $instance   Created object.
410
     * @param array  $parameters Parameters which been passed with created instance.
411
     *
412
     * @return object
413
     */
414
    protected function registerInstance($instance, array $parameters)
415 39
    {
416
        //Declarative singletons (only when class received via direct get)
417
        if (empty($parameters) && $instance instanceof SingletonInterface) {
418 39
            $alias = get_class($instance);
419 38
420
            if (!isset($this->bindings[$alias])) {
421 6
                $this->bindings[$alias] = $instance;
422
            }
423 1
        }
424
425
        //Your code can go here (for example LoggerAwareInterface, custom hydration and etc)
426
427 38
        return $instance;
428
    }
429
430 25
    /**
431
     * @param string      $alias
432
     * @param mixed       $target Value binded by user.
433
     * @param array       $parameters
434
     * @param string|null $context
435
     *
436
     * @return mixed|null|object
437
     *
438
     * @throws \Spiral\Core\Exceptions\Container\ContainerException
439
     */
440
    private function evaluateBinding(
441
        string $alias,
442 25
        $target,
443
        array $parameters,
444
        string $context = null
445 25
    ) {
446 1
        if (is_string($target)) {
447
            //Reference
448 1
            return $this->make($target, $parameters, $context);
449 1
        }
450
451
        if ($target instanceof Autowire) {
452
            return $target->resolve($this, $parameters);
453
        }
454
455 25
        if ($target instanceof \Closure) {
456
            $reflection = new \ReflectionFunction($target);
457
458
            //Invoking Closure with resolved arguments
459
            return $reflection->invokeArgs(
460
                $this->resolveArguments($reflection, $parameters, $context)
461
            );
462
        }
463
464
        if (is_array($target) && isset($target[1])) {
465
            //In a form of resolver and method
466
            list($resolver, $method) = $target;
467
468
            //Resolver instance (i.e. [ClassName::class, 'method'])
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
469 38
            $resolver = $this->get($resolver);
470
            $method = new \ReflectionMethod($resolver, $method);
471 38
            $method->setAccessible(true);
472
473
            //Invoking factory method with resolved arguments
474 38
            return $method->invokeArgs(
475 11
                $resolver,
476
                $this->resolveArguments($method, $parameters, $context)
477 6
            );
478 1
        }
479
480
        throw new ContainerException("Invalid binding for '{$alias}'");
481 5
    }
482
483
    /**
484 28
     * Create instance of desired class.
485 1
     *
486
     * @param string      $class
487
     * @param array       $parameters Constructor parameters.
488 27
     * @param string|null $context
489
     *
490 21
     * @return object
491 21
     *
492
     * @throws ContainerException
493
     */
494
    private function createInstance(string $class, array $parameters, string $context = null)
495 8
    {
496
        $reflection = new \ReflectionClass($class);
497
498 20
        //We have to construct class using external injector when we know exact context
499
        if (empty($parameters) && $this->hasInjector($reflection)) {
500
            $instance = $this->getInjector($reflection)->createInjection($reflection, $context);
501
502
            if (!$reflection->isInstance($instance)) {
503
                throw new InjectionException("Invalid injection response for '{$reflection->getName()}'");
504
            }
505
506
            return $instance;
507
        }
508 11
509
        if (!$reflection->isInstantiable()) {
510 11
            throw new ContainerException("Class '{$class}' can not be constructed");
511
        }
512 3
513
        if (!empty($constructor = $reflection->getConstructor())) {
514
            //Using constructor with resolved arguments
515 8
            $instance = $reflection->newInstanceArgs(
516
                $this->resolveArguments($constructor, $parameters)
517
            );
518 8
        } else {
519 2
            //No constructor specified
520 2
            $instance = $reflection->newInstance();
521
        }
522
523
        return $instance;
524 6
    }
525
526
    /**
527
     * Get injector associated with given class.
528
     *
529
     * @param \ReflectionClass $reflection
530
     *
531
     * @return InjectorInterface
532
     */
533
    private function getInjector(\ReflectionClass $reflection): InjectorInterface
534
    {
535
        if (isset($this->injectors[$reflection->getName()])) {
536 17
            //Stated directly
537
            $injector = $this->get($this->injectors[$reflection->getName()]);
538
        } else {
539
            //Auto-injection!
540
            $injector = $this->get($reflection->getConstant('INJECTOR'));
541 17
        }
542 2
543 1
        if (!$injector instanceof InjectorInterface) {
544
            throw new InjectionException(
545
                "Class '" . get_class($injector) . "' must be an instance of InjectorInterface for '{$reflection->getName()}'"
546 1
            );
547
        }
548
549 16
        return $injector;
550
    }
551 16
552 1
    /**
553
     * Assert that given value are matched parameter type.
554
     *
555 16
     * @param \ReflectionParameter        $parameter
556 2
     * @param \ReflectionFunctionAbstract $context
557
     * @param mixed                       $value
558
     *
559 16
     * @throws ArgumentException
560 1
     */
561
    private function assertType(
562 16
        \ReflectionParameter $parameter,
563
        \ReflectionFunctionAbstract $context,
564
        $value
565
    ) {
566
        if (is_null($value)) {
567
            if (
568
            	!$parameter->isOptional()
569
				&& !($parameter->isDefaultValueAvailable() && $parameter->getDefaultValue() === null)
570
			) {
571
                throw new ArgumentException($parameter, $context);
572
            }
573
574
            return;
575
        }
576
577
        $type = $parameter->getType();
578
579
        if ($type == 'array' && !is_array($value)) {
580
            throw new ArgumentException($parameter, $context);
581
        }
582
583
        if (($type == 'int' || $type == 'float') && !is_numeric($value)) {
584
            throw new ArgumentException($parameter, $context);
585
        }
586
587
        if ($type == 'bool' && !is_bool($value) && !is_numeric($value)) {
588
            throw new ArgumentException($parameter, $context);
589
        }
590
    }
591
}