Passed
Pull Request — master (#240)
by Dmitriy
02:32
created

Container::isResolvable()   D

Complexity

Conditions 25
Paths 26

Size

Total Lines 97
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 46.7535

Importance

Changes 11
Bugs 0 Features 0
Metric Value
cc 25
eloc 50
c 11
b 0
f 0
nc 26
nop 1
dl 0
loc 97
ccs 33
cts 49
cp 0.6735
crap 46.7535
rs 4.1666

How to fix   Long Method    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
declare(strict_types=1);
4
5
namespace Yiisoft\Di;
6
7
use Closure;
8
use Psr\Container\ContainerInterface;
9
use ReflectionClass;
10
use ReflectionException;
11
use ReflectionNamedType;
12
use ReflectionUnionType;
13
use Yiisoft\Di\Contracts\ServiceProviderInterface;
14
use Yiisoft\Factory\Definition\ArrayDefinition;
15
use Yiisoft\Factory\Definition\DefinitionValidator;
16
use Yiisoft\Factory\Exception\CircularReferenceException;
17
use Yiisoft\Factory\Exception\InvalidConfigException;
18
use Yiisoft\Factory\Exception\NotFoundException;
19
use Yiisoft\Factory\Exception\NotInstantiableException;
20
21
use function array_key_exists;
22
use function array_keys;
23
use function class_exists;
24
use function get_class;
25
use function implode;
26
use function in_array;
27
use function is_array;
28
use function is_object;
29
use function is_string;
30
31
/**
32
 * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
33
 */
34
final class Container implements ContainerInterface
35
{
36
    private const META_TAGS = 'tags';
37
    private const META_RESET = 'reset';
38
    private const ALLOWED_META = [self::META_TAGS, self::META_RESET];
39
40
    /**
41
     * @var array object definitions indexed by their types
42
     */
43
    private array $definitions = [];
44
    /**
45
     * @var array used to collect ids instantiated during build
46
     * to detect circular references
47
     */
48
    private array $building = [];
49
    private array $resolvableBuilding = [];
50
51
    /**
52
     * @var bool $validate Validate definitions when set
53
     */
54
    private bool $validate;
55
56
    /**
57
     * @var object[]
58
     */
59
    private array $instances = [];
60
61
    private array $tags;
62
63
    private array $resetters = [];
64
    /** @psalm-suppress PropertyNotSetInConstructor */
65
    private DependencyResolver $dependencyResolver;
66
67
    /**
68
     * Container constructor.
69
     *
70
     * @param array $definitions Definitions to put into container.
71
     * @param array $providers Service providers to get definitions from.
72
     * lookup to when resolving dependencies. If provided the current container
73
     * is no longer queried for dependencies.
74
     *
75
     * @throws InvalidConfigException
76
     *
77
     * @psalm-suppress PropertyNotSetInConstructor
78
     */
79 96
    public function __construct(
80
        array $definitions = [],
81
        array $providers = [],
82
        array $tags = [],
83
        bool $validate = true
84
    ) {
85 96
        $this->tags = $tags;
86 96
        $this->validate = $validate;
87 96
        $this->setDefaultDefinitions();
88 96
        $this->setMultiple($definitions);
89 90
        $this->addProviders($providers);
90 89
    }
91
92
    /**
93
     * Returns a value indicating whether the container has the definition of the specified name.
94
     *
95
     * @param string $id class name, interface name or alias name
96
     *
97
     * @return bool whether the container is able to provide instance of class specified.
98
     *
99
     * @see set()
100
     */
101 29
    public function has($id): bool
102
    {
103
        /** @psalm-suppress  DocblockTypeContradiction */
104 29
        if (!is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
105
            return false;
106
        }
107
108 29
        if ($this->isTagAlias($id)) {
109 2
            $tag = substr($id, 4);
110 2
            return isset($this->tags[$tag]);
111
        }
112
113
        try {
114 27
            return $this->isResolvable($id);
115 1
        } catch (CircularReferenceException $e) {
116 1
            return false;
117
        }
118
    }
119
120
    /**
121
     * Returns an instance by either interface name or alias.
122
     *
123
     * Same instance of the class will be returned each time this method is called.
124
     *
125
     * @param string $id The interface or an alias name that was previously registered.
126
     *
127
     * @throws CircularReferenceException
128
     * @throws InvalidConfigException
129
     * @throws NotFoundException
130
     * @throws NotInstantiableException
131
     *
132
     * @return mixed|object An instance of the requested interface.
133
     *
134
     * @psalm-template T
135
     * @psalm-param string|class-string<T> $id
136
     * @psalm-return ($id is class-string ? T : mixed)
137
     */
138 83
    public function get($id)
139
    {
140
        /** @psalm-suppress TypeDoesNotContainType */
141 83
        if (!is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
142
            throw new \RuntimeException("Id must be string, {$this->getVariableType($id)} given.");
143
        }
144
145 83
        if ($id === StateResetter::class && !isset($this->definitions[$id])) {
146 4
            $resetters = [];
147 4
            foreach ($this->resetters as $serviceId => $callback) {
148 4
                if (isset($this->instances[$serviceId])) {
149 4
                    $resetters[] = $callback->bindTo($this->instances[$serviceId], get_class($this->instances[$serviceId]));
150
                }
151
            }
152 4
            return new StateResetter($resetters, $this);
153
        }
154
155 83
        if (!array_key_exists($id, $this->instances)) {
156 83
            $this->instances[$id] = $this->build($id);
157
        }
158
159 79
        return $this->instances[$id];
160
    }
161
162
    /**
163
     * @param string $id class name, interface name or alias name
164
     *
165
     * @throws CircularReferenceException
166
     */
167 58
    private function isResolvable(string $id): bool
168
    {
169 58
        if (isset($this->definitions[$id]) || $id === StateResetter::class) {
170 27
            return true;
171
        }
172
173 55
        if (!class_exists($id)) {
174 12
            return false;
175
        }
176
177 50
        if (isset($this->resolvableBuilding[$id])) {
178 3
            throw new CircularReferenceException(sprintf(
179 3
                'Circular reference to "%s" detected while building: %s.',
180
                $id,
181 3
                implode(', ', array_keys($this->resolvableBuilding))
182
            ));
183
        }
184
185
        try {
186 50
            $reflectionClass = new ReflectionClass($id);
187
        } catch (ReflectionException $e) {
188
            return false;
189
        }
190
191 50
        if (!$reflectionClass->isInstantiable()) {
192
            return false;
193
        }
194
195 50
        $constructor = $reflectionClass->getConstructor();
196
197 50
        if ($constructor === null) {
198 5
            return true;
199
        }
200
201 48
        $isResolvable = true;
202 48
        $this->resolvableBuilding[$id] = 1;
203
204 48
        foreach ($constructor->getParameters() as $parameter) {
205 48
            $type = $parameter->getType();
206
207 48
            if ($parameter->isVariadic() || $parameter->isOptional()) {
208 43
                break;
209
            }
210
211
            /**
212
             * @var ReflectionNamedType|ReflectionUnionType|null $type
213
             * @psalm-suppress RedundantConditionGivenDocblockType
214
             * @psalm-suppress UndefinedClass
215
             */
216 18
            if ($type === null || !$type instanceof ReflectionUnionType && $type->isBuiltin()) {
217 4
                $isResolvable = false;
218 4
                break;
219
            }
220
221
            // PHP 8 union type is used as type hint
222
            /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
223 18
            if ($type instanceof ReflectionUnionType) {
224
                $isUnionTypeResolvable = false;
225
                /** @var ReflectionNamedType $unionType */
226
                foreach ($type->getTypes() as $unionType) {
227
                    if (!$unionType->isBuiltin()) {
228
                        $typeName = $unionType->getName();
229
                        if ($typeName === 'self') {
230
                            continue;
231
                        }
232
                        if ($this->isResolvable($typeName)) {
233
                            $isUnionTypeResolvable = true;
234
                            break;
235
                        }
236
                    }
237
                }
238
239
                $isResolvable = $isUnionTypeResolvable;
240
                if (!$isResolvable) {
241
                    break;
242
                }
243
                continue;
244
            }
245
246
            /** @var ReflectionNamedType|null $type */
247
            // Our parameter has a class type hint
248 18
            if ($type !== null && !$type->isBuiltin()) {
249 18
                $typeName = $type->getName();
250
251 18
                if ($typeName === 'self' || !$this->isResolvable($typeName)) {
252 6
                    $isResolvable = false;
253 6
                    break;
254
                }
255
            }
256
        }
257
258 45
        if ($isResolvable) {
259 43
            $this->definitions[$id] = $id;
260
        }
261 45
        unset($this->resolvableBuilding[$id]);
262
263 45
        return $isResolvable;
264
    }
265
266
    /**
267
     * Sets a definition to the container. Definition may be defined multiple ways.
268
     *
269
     * @param string $id
270
     * @param mixed $definition
271
     *
272
     * @throws InvalidConfigException
273
     *
274
     * @see `DefinitionNormalizer::normalize()`
275
     */
276 96
    private function set(string $id, $definition): void
277
    {
278 96
        [$definition, $meta] = DefinitionParser::parse($definition);
279 96
        if ($this->validate) {
280 96
            $this->validateDefinition($definition, $id);
281 96
            $this->validateMeta($meta);
282
        }
283
284 96
        if (isset($meta[self::META_TAGS])) {
285 8
            if ($this->validate) {
286 8
                $this->validateTags($meta[self::META_TAGS]);
287
            }
288 8
            $this->setTags($id, $meta[self::META_TAGS]);
289
        }
290 96
        if (isset($meta[self::META_RESET])) {
291 5
            $this->setResetter($id, $meta[self::META_RESET]);
292
        }
293
294 96
        unset($this->instances[$id]);
295 96
        $this->definitions[$id] = $definition;
296 96
    }
297
298
    /**
299
     * Sets multiple definitions at once.
300
     *
301
     * @param array $config definitions indexed by their ids
302
     *
303
     * @throws InvalidConfigException
304
     */
305 96
    private function setMultiple(array $config): void
306
    {
307 96
        foreach ($config as $id => $definition) {
308 88
            if ($this->validate && !is_string($id)) {
309 1
                throw new InvalidConfigException(sprintf('Key must be a string. %s given.', $this->getVariableType($id)));
310
            }
311 87
            $this->set($id, $definition);
312
        }
313 90
    }
314
315 96
    private function setDefaultDefinitions(): void
316
    {
317 96
        $this->set(ContainerInterface::class, $this);
318 96
    }
319
320
    /**
321
     * @param mixed $definition
322
     *
323
     * @throws InvalidConfigException
324
     */
325 96
    private function validateDefinition($definition, ?string $id = null): void
326
    {
327 96
        if (is_array($definition) && isset($definition[DefinitionParser::IS_PREPARED_ARRAY_DEFINITION_DATA])) {
328 39
            $class = $definition['class'];
329 39
            $constructorArguments = $definition['__construct()'];
330 39
            $methodsAndProperties = $definition['methodsAndProperties'];
331 39
            $definition = array_merge(
332 39
                $class === null ? [] : [ArrayDefinition::CLASS_NAME => $class],
333 39
                [ArrayDefinition::CONSTRUCTOR => $constructorArguments],
334
                $methodsAndProperties,
335
            );
336
        }
337
338 96
        if ($definition instanceof ExtensibleService) {
339
            throw new InvalidConfigException('Invalid definition. ExtensibleService is only allowed in provider extensions.');
340
        }
341
342 96
        DefinitionValidator::validate($definition, $id);
343 96
    }
344
345
    /**
346
     * @throws InvalidConfigException
347
     */
348 96
    private function validateMeta(array $meta): void
349
    {
350 96
        foreach ($meta as $key => $_value) {
351 16
            if (!in_array($key, self::ALLOWED_META, true)) {
352 3
                throw new InvalidConfigException(
353 3
                    sprintf(
354 3
                        'Invalid definition: metadata "%s" is not allowed. Did you mean "%s()" or "$%s"?',
355
                        $key,
356
                        $key,
357
                        $key,
358
                    )
359
                );
360
            }
361
        }
362 96
    }
363
364 8
    private function validateTags(array $tags): void
365
    {
366 8
        foreach ($tags as $tag) {
367 8
            if (!is_string($tag)) {
368
                throw new InvalidConfigException('Invalid tag. Expected a string, got ' . var_export($tag, true) . '.');
369
            }
370
        }
371 8
    }
372
373 8
    private function setTags(string $id, array $tags): void
374
    {
375 8
        foreach ($tags as $tag) {
376 8
            if (!isset($this->tags[$tag]) || !in_array($id, $this->tags[$tag], true)) {
377 8
                $this->tags[$tag][] = $id;
378
            }
379
        }
380 8
    }
381
382 4
    private function setResetter(string $id, Closure $resetter): void
383
    {
384 4
        $this->resetters[$id] = $resetter;
385 4
    }
386
387
    /**
388
     * Creates new instance by either interface name or alias.
389
     *
390
     * @param string $id The interface or an alias name that was previously registered.
391
     *
392
     * @throws CircularReferenceException
393
     * @throws InvalidConfigException
394
     * @throws NotFoundException
395
     *
396
     * @return mixed|object New built instance of the specified class.
397
     *
398
     * @internal
399
     */
400 83
    private function build(string $id)
401
    {
402 83
        if ($this->isTagAlias($id)) {
403 9
            return $this->getTaggedServices($id);
404
        }
405
406 82
        if (isset($this->building[$id])) {
407 78
            if ($id === ContainerInterface::class) {
408 78
                return $this;
409
            }
410 4
            throw new CircularReferenceException(sprintf(
411 4
                'Circular reference to "%s" detected while building: %s.',
412
                $id,
413 4
                implode(',', array_keys($this->building))
414
            ));
415
        }
416
417 82
        $this->building[$id] = 1;
418
        try {
419 82
            $object = $this->buildInternal($id);
420 78
        } finally {
421 82
            unset($this->building[$id]);
422
        }
423
424 78
        return $object;
425
    }
426
427 89
    private function isTagAlias(string $id): bool
428
    {
429 89
        return strpos($id, 'tag@') === 0;
430
    }
431
432 9
    private function getTaggedServices(string $tagAlias): array
433
    {
434 9
        $tag = substr($tagAlias, 4);
435 9
        $services = [];
436 9
        if (isset($this->tags[$tag])) {
437 8
            foreach ($this->tags[$tag] as $service) {
438 8
                $services[] = $this->get($service);
439
            }
440
        }
441
442 9
        return $services;
443
    }
444
445
    /**
446
     * @param string $id
447
     *
448
     * @throws InvalidConfigException
449
     * @throws NotFoundException
450
     *
451
     * @return mixed|object
452
     */
453 82
    private function buildInternal(string $id)
454
    {
455 82
        if (!isset($this->definitions[$id])) {
456 46
            return $this->buildPrimitive($id);
457
        }
458 78
        $definition = DefinitionNormalizer::normalize($this->definitions[$id], $id);
459
        /** @psalm-suppress RedundantPropertyInitializationCheck */
460 78
        $this->dependencyResolver ??= new DependencyResolver($this->get(ContainerInterface::class));
461
462 78
        return $definition->resolve($this->dependencyResolver);
463
    }
464
465
    /**
466
     * @param string $class
467
     *
468
     * @throws InvalidConfigException
469
     * @throws NotFoundException
470
     *
471
     * @return mixed|object
472
     */
473 46
    private function buildPrimitive(string $class)
474
    {
475 46
        if ($this->isResolvable($class)) {
476
            /** @psalm-suppress ArgumentTypeCoercion */
477 41
            $definition = ArrayDefinition::fromPreparedData($class);
478
            /** @psalm-suppress RedundantPropertyInitializationCheck */
479 41
            $this->dependencyResolver ??= new DependencyResolver($this->get(ContainerInterface::class));
480
481 41
            return $definition->resolve($this->dependencyResolver);
482
        }
483
484 3
        throw new NotFoundException($class);
485
    }
486
487 90
    private function addProviders(array $providers): void
488
    {
489 90
        $extensions = [];
490 90
        foreach ($providers as $provider) {
491 5
            $providerInstance = $this->buildProvider($provider);
492 5
            $extensions[] = $providerInstance->getExtensions();
493 5
            $this->addProviderDefinitions($providerInstance);
494
        }
495
496 90
        foreach ($extensions as $providerExtensions) {
497 5
            foreach ($providerExtensions as $id => $extension) {
498 4
                if (!isset($this->definitions[$id])) {
499 1
                    throw new InvalidConfigException("Extended service \"$id\" doesn't exist.");
500
                }
501
502 3
                if (!$this->definitions[$id] instanceof ExtensibleService) {
503 3
                    $this->definitions[$id] = new ExtensibleService($this->definitions[$id]);
504
                }
505
506 3
                $this->definitions[$id]->addExtension($extension);
507
            }
508
        }
509 89
    }
510
511
    /**
512
     * Adds service provider definitions to the container.
513
     *
514
     * @param object $provider
515
     *
516
     * @throws InvalidConfigException
517
     * @throws NotInstantiableException
518
     *
519
     * @see ServiceProviderInterface
520
     */
521 5
    private function addProviderDefinitions($provider): void
522
    {
523 5
        $definitions = $provider->getDefinitions();
524 5
        $this->setMultiple($definitions);
525 5
    }
526
527
    /**
528
     * Builds service provider by definition.
529
     *
530
     * @param mixed $provider Class name or instance of provider.
531
     *
532
     * @throws InvalidConfigException If provider argument is not valid.
533
     *
534
     * @return ServiceProviderInterface Instance of service provider.
535
     *
536
     * @psalm-suppress MoreSpecificReturnType
537
     */
538 5
    private function buildProvider($provider): ServiceProviderInterface
539
    {
540 5
        if ($this->validate && !(is_string($provider) || is_object($provider) && $provider instanceof ServiceProviderInterface)) {
541
            throw new InvalidConfigException(
542
                sprintf(
543
                    'Service provider should be a class name or an instance of %s. %s given.',
544
                    ServiceProviderInterface::class,
545
                    $this->getVariableType($provider)
546
                )
547
            );
548
        }
549
550 5
        $providerInstance = is_object($provider) ? $provider : new $provider();
551 5
        if (!$providerInstance instanceof ServiceProviderInterface) {
552
            throw new InvalidConfigException(
553
                sprintf(
554
                    'Service provider should be an instance of %s. %s given.',
555
                    ServiceProviderInterface::class,
556
                    $this->getVariableType($providerInstance)
557
                )
558
            );
559
        }
560
561
        /**
562
         * @psalm-suppress LessSpecificReturnStatement
563
         */
564 5
        return $providerInstance;
565
    }
566
567
    /**
568
     * @param mixed $variable
569
     */
570 1
    private function getVariableType($variable): string
571
    {
572 1
        if (is_object($variable)) {
573
            return get_class($variable);
574
        }
575
576 1
        return gettype($variable);
577
    }
578
}
579