Completed
Push — master ( 682385...a29af3 )
by Anton
10s
created

Container::evaluateBinding()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 22
nc 5
nop 4
dl 0
loc 42
ccs 11
cts 11
cp 1
crap 6
rs 8.439
c 0
b 0
f 0
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
        //Direct bypass to construct, i might think about this option... or not.
125 34
        return $this->make($alias, [], $context);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     *
131
     * @param string|null $context Related to parameter caused injection if any.
132
     */
133 49
    final public function make(string $alias, $parameters = [], string $context = null)
134
    {
135 49
        if (!isset($this->bindings[$alias])) {
136
            //No direct instructions how to construct class, make is automatically
137 39
            return $this->autowire($alias, $parameters, $context);
138
        }
139
140 31
        if (is_object($binding = $this->bindings[$alias])) {
141
            //When binding is instance, assuming singleton
142 16
            return $binding;
143
        }
144
145 21
        if (is_string($binding)) {
146
            //Binding is pointing to something else
147 11
            return $this->make($binding, $parameters, $context);
148
        }
149
150 11
        $instance = $this->evaluateBinding($alias, $binding[0], $parameters, $context);
151
152 1
        if ($binding[1]) {
153 11
            //Indicates singleton
154 7
            $this->bindings[$alias] = $instance;
155
        }
156
157 7
        return $instance;
158 7
    }
159
160 5
    /**
161
     * {@inheritdoc}
162 4
     *
163
     * @param string $context
164
     */
165 4
    final public function resolveArguments(
166 4
        ContextFunction $reflection,
167 4
        array $parameters = [],
168
        string $context = null
169
    ): array {
170 4
        $arguments = [];
171
        foreach ($reflection->getParameters() as $parameter) {
172 4
            try {
173
                //Information we need to know about argument in order to resolve it's value
174
                $name = $parameter->getName();
175
                $class = $parameter->getClass();
176 1
            } catch (\Throwable $e) {
177
                //Possibly invalid class definition or syntax error
178
                throw new ContainerException($e->getMessage(), $e->getCode(), $e);
179 10
            }
180
181 5
            if (isset($parameters[$name]) && is_object($parameters[$name])) {
182
                if ($parameters[$name] instanceof Autowire) {
183
                    //Supplied by user as late dependency
184 10
                    $arguments[] = $parameters[$name]->resolve($this);
185
                } else {
186 2
                    //Supplied by user as object
187
                    $arguments[] = $parameters[$name];
188
                }
189 8
                continue;
190
            }
191
192
            //No declared type or scalar type or array
193
            if (empty($class)) {
194
                //Provided from outside
195
                if (array_key_exists($name, $parameters)) {
196
                    //Make sure it's properly typed
197 29
                    $this->assertType($parameter, $reflection, $parameters[$name]);
198
                    $arguments[] = $parameters[$name];
199
                    continue;
200
                }
201
202 29
                if ($parameter->isDefaultValueAvailable()) {
203 29
                    //Default value
204
                    $arguments[] = $parameter->getDefaultValue();
205
                    continue;
206 24
                }
207 24
208 1
                //Unable to resolve scalar argument value
209
                throw new ArgumentException($parameter, $reflection);
210 1
            }
211
212
            try {
213 23
                //Requesting for contextual dependency
214 1
                $arguments[] = $this->get($class->getName(), $name);
215
216 1
                continue;
217
            } catch (AutowireException $e) {
218
                if ($parameter->isOptional()) {
219
                    //This is optional dependency, skip
220
                    $arguments[] = null;
221 1
                    continue;
222
                }
223
224
                throw $e;
225 23
            }
226
        }
227 21
228
        return $arguments;
229 17
    }
230 16
231 16
    /**
232
     * Bind value resolver to container alias. Resolver can be class name (will be constructed
233
     * for each method call), function array or Closure (executed every call). Only object resolvers
234 9
     * supported by this method.
235
     *
236 8
     * @param string                $alias
237 8
     * @param string|array|callable $resolver
238
     *
239
     * @return self
240
     */
241 1
    final public function bind(string $alias, $resolver): Container
242
    {
243
        if (is_array($resolver) || $resolver instanceof \Closure || $resolver instanceof Autowire) {
244
            //Array means = execute me, false = not singleton
245
            $this->bindings[$alias] = [$resolver, false];
246 7
247
            return $this;
248 5
        }
249 2
250 2
        $this->bindings[$alias] = $resolver;
251
252 1
        return $this;
253 1
    }
254
255
    /**
256 1
     * Bind value resolver to container alias to be executed as cached. Resolver can be class name
257
     * (will be constructed only once), function array or Closure (executed only once call).
258
     *
259
     * @param string                $alias
260 21
     * @param string|array|callable $resolver
261
     *
262
     * @return self
263
     */
264
    final public function bindSingleton(string $alias, $resolver): Container
265
    {
266
        if (is_object($resolver) && !$resolver instanceof \Closure && !$resolver instanceof Autowire) {
267
            //Direct binding to an instance
268
            $this->bindings[$alias] = $resolver;
269
270
            return $this;
271
        }
272
273 25
        $this->bindings[$alias] = [$resolver, true];
274
275 25
        return $this;
276
    }
277 7
278
    /**
279 7
     * Specify binding which has to be used for class injection.
280
     *
281
     * @param string        $class
282 19
     * @param string|object $injector
283
     *
284 19
     * @return self
285
     */
286
    public function bindInjector(string $class, $injector): Container
287
    {
288
        if (!is_string($injector)) {
289
            throw new \InvalidArgumentException('Injector can only be set as string binding');
290
        }
291
292
        $this->injectors[$class] = $injector;
293
294
        return $this;
295
    }
296 9
297
    /**
298 9
     * Check if given class has associated injector.
299
     *
300 4
     * @param \ReflectionClass $reflection
301
     *
302 4
     * @return bool
303
     */
304
    public function hasInjector(\ReflectionClass $reflection): bool
305 5
    {
306
        if (isset($this->injectors[$reflection->getName()])) {
307 5
            return true;
308
        }
309
310
        //Auto injection!
311
        return $reflection->isSubclassOf(InjectableInterface::class);
312
    }
313
314
    /**
315
     * Check if alias points to constructed instance (singleton).
316
     *
317
     * @param string $alias
318 5
     *
319
     * @return bool
320 5
     */
321 1
    final public function hasInstance(string $alias): bool
322
    {
323
        if (!$this->has($alias)) {
324 4
            return false;
325
        }
326 4
327
        while (isset($this->bindings[$alias]) && is_string($this->bindings[$alias])) {
328
            //Checking alias tree
329
            $alias = $this->bindings[$alias];
330
        }
331
332
        return isset($this->bindings[$alias]) && is_object($this->bindings[$alias]);
333
    }
334
335
    /**
336 25
     * @param string $alias
337
     */
338 25
    final public function removeBinding(string $alias)
339 3
    {
340
        unset($this->bindings[$alias]);
341
    }
342
343 23
    /**
344
     * @param string $class
345
     */
346
    final public function removeInjector(string $class)
347
    {
348
        unset($this->injectors[$class]);
349
    }
350
351
    /**
352
     * Every declared Container binding. Must not be used in production code due container format is
353 2
     * vary.
354
     *
355 2
     * @return array
356 2
     */
357
    final public function getBindings(): array
358
    {
359 2
        return $this->bindings;
360
    }
361 1
362
    /**
363
     * Every binded injector.
364 2
     *
365
     * @return array
366
     */
367
    final public function getInjectors(): array
368
    {
369
        return $this->injectors;
370 1
    }
371
372 1
    /**
373 1
     * Automatically create class.
374
     *
375
     * @param string $class
376
     * @param array  $parameters
377
     * @param string $context
378 1
     *
379
     * @return object
380 1
     *
381 1
     * @throws AutowireException
382
     */
383
    final protected function autowire(string $class, array $parameters, string $context = null)
384
    {
385
        try {
386
            if (!class_exists($class)) {
387
                throw new NotFoundException("Undefined class or binding '{$class}'");
388
            }
389 1
        } catch (\Error $e) {
390
            //Issues with syntax or class definition
391 1
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
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 \Spiral\Core\Exceptions\Container\ContainerException
435
     */
436
    private function evaluateBinding(
437
        string $alias,
438
        $target,
439
        array $parameters,
440
        string $context = null
441
    ) {
442 25
        if (is_string($target)) {
443
            //Reference
444
            return $this->make($target, $parameters, $context);
445 25
        }
446 1
447
        if ($target instanceof Autowire) {
448 1
            return $target->resolve($this, $parameters);
449 1
        }
450
451
        if ($target instanceof \Closure) {
452
            $reflection = new \ReflectionFunction($target);
453
454
            //Invoking Closure with resolved arguments
455 25
            return $reflection->invokeArgs(
456
                $this->resolveArguments($reflection, $parameters, $context)
457
            );
458
        }
459
460
        if (is_array($target) && isset($target[1])) {
461
            //In a form of resolver and method
462
            list($resolver, $method) = $target;
463
464
            //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...
465
            $resolver = $this->get($resolver);
466
            $method = new \ReflectionMethod($resolver, $method);
467
            $method->setAccessible(true);
468
469 38
            //Invoking factory method with resolved arguments
470
            return $method->invokeArgs(
471 38
                $resolver,
472
                $this->resolveArguments($method, $parameters, $context)
473
            );
474 38
        }
475 11
476
        throw new ContainerException("Invalid binding for '{$alias}'");
477 6
    }
478 1
479
    /**
480
     * Create instance of desired class.
481 5
     *
482
     * @param string      $class
483
     * @param array       $parameters Constructor parameters.
484 28
     * @param string|null $context
485 1
     *
486
     * @return object
487
     *
488 27
     * @throws ContainerException
489
     */
490 21
    private function createInstance(string $class, array $parameters, string $context = null)
491 21
    {
492
        $reflection = new \ReflectionClass($class);
493
494
        //We have to construct class using external injector when we know exact context
495 8
        if (empty($parameters) && $this->hasInjector($reflection)) {
496
            $instance = $this->getInjector($reflection)->createInjection($reflection, $context);
497
498 20
            if (!$reflection->isInstance($instance)) {
499
                throw new InjectionException("Invalid injection response for '{$reflection->getName()}'");
500
            }
501
502
            return $instance;
503
        }
504
505
        if (!$reflection->isInstantiable()) {
506
            throw new ContainerException("Class '{$class}' can not be constructed");
507
        }
508 11
509
        if (!empty($constructor = $reflection->getConstructor())) {
510 11
            //Using constructor with resolved arguments
511
            $instance = $reflection->newInstanceArgs(
512 3
                $this->resolveArguments($constructor, $parameters)
513
            );
514
        } else {
515 8
            //No constructor specified
516
            $instance = $reflection->newInstance();
517
        }
518 8
519 2
        return $instance;
520 2
    }
521
522
    /**
523
     * Get injector associated with given class.
524 6
     *
525
     * @param \ReflectionClass $reflection
526
     *
527
     * @return InjectorInterface
528
     */
529
    private function getInjector(\ReflectionClass $reflection): InjectorInterface
530
    {
531
        if (isset($this->injectors[$reflection->getName()])) {
532
            //Stated directly
533
            $injector = $this->get($this->injectors[$reflection->getName()]);
534
        } else {
535
            //Auto-injection!
536 17
            $injector = $this->get($reflection->getConstant('INJECTOR'));
537
        }
538
539
        if (!$injector instanceof InjectorInterface) {
540
            throw new InjectionException(
541 17
                "Class '" . get_class($injector) . "' must be an instance of InjectorInterface for '{$reflection->getName()}'"
542 2
            );
543 1
        }
544
545
        return $injector;
546 1
    }
547
548
    /**
549 16
     * Assert that given value are matched parameter type.
550
     *
551 16
     * @param \ReflectionParameter        $parameter
552 1
     * @param \ReflectionFunctionAbstract $context
553
     * @param mixed                       $value
554
     *
555 16
     * @throws ArgumentException
556 2
     */
557
    private function assertType(
558
        \ReflectionParameter $parameter,
559 16
        \ReflectionFunctionAbstract $context,
560 1
        $value
561
    ) {
562 16
        if (is_null($value)) {
563
            if (!$parameter->isOptional()) {
564
                throw new ArgumentException($parameter, $context);
565
            }
566
567
            return;
568
        }
569
570
        $type = $parameter->getType();
571
572
        if ($type == 'array' && !is_array($value)) {
573
            throw new ArgumentException($parameter, $context);
574
        }
575
576
        if (($type == 'int' || $type == 'float') && !is_numeric($value)) {
577
            throw new ArgumentException($parameter, $context);
578
        }
579
580
        if ($type == 'bool' && !is_bool($value) && !is_numeric($value)) {
581
            throw new ArgumentException($parameter, $context);
582
        }
583
    }
584
}