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

Container::isResolvable()   D

Complexity

Conditions 24
Paths 18

Size

Total Lines 95
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 39.2276

Importance

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