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

Container::isResolvable()   D

Complexity

Conditions 25
Paths 26

Size

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