Passed
Push — master ( 6a2fee...c38029 )
by Alexander
05:10 queued 03:00
created

DefinitionStorage   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 183
Duplicated Lines 0 %

Test Coverage

Coverage 54.32%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 79
c 1
b 0
f 0
dl 0
loc 183
ccs 44
cts 81
cp 0.5432
rs 9.52
wmc 36

7 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 6 2
A getBuildStack() 0 3 1
A setDelegateContainer() 0 3 1
A has() 0 4 1
A set() 0 3 1
A __construct() 0 3 1
D isResolvable() 0 124 29
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions\Infrastructure;
6
7
use Psr\Container\ContainerInterface;
8
use ReflectionNamedType;
9
use ReflectionUnionType;
10
use RuntimeException;
11
use Yiisoft\Definitions\Exception\CircularReferenceException;
12
use Yiisoft\Definitions\Infrastructure\DefinitionExtractor;
13
14
/**
15
 * Stores service definitions and checks if a definition could be instantiated.
16
 *
17
 * @internal
18
 */
19
final class DefinitionStorage
20
{
21
    private array $definitions;
22
    private array $buildStack = [];
23
    /** @psalm-suppress  PropertyNotSetInConstructor */
24
    private ?ContainerInterface $delegateContainer = null;
25
26 8
    public function __construct(array $definitions = [])
27
    {
28 8
        $this->definitions = $definitions;
29 8
    }
30
31 1
    public function setDelegateContainer(ContainerInterface $delegateContainer): void
32
    {
33 1
        $this->delegateContainer = $delegateContainer;
34 1
    }
35
36
    /**
37
     * Checks if there is a definition with ID specified and that it can be created.
38
     *
39
     * @param string $id class name, interface name or alias name
40
     *
41
     * @throws CircularReferenceException
42
     */
43 8
    public function has(string $id): bool
44
    {
45 8
        $this->buildStack = [];
46 8
        return $this->isResolvable($id, []);
47
    }
48
49 8
    public function getBuildStack(): array
50
    {
51 8
        return $this->buildStack;
52
    }
53
54
    /**
55
     * Get a definition with a given ID.
56
     *
57
     * @return mixed|object Definition with a given ID.
58
     */
59
    public function get(string $id)
60
    {
61
        if (!isset($this->definitions[$id])) {
62
            throw new RuntimeException("Service $id doesn't exist in DefinitionStorage.");
63
        }
64
        return $this->definitions[$id];
65
    }
66
67
    /**
68
     * Set a definition.
69
     *
70
     * @param string $id ID to set definition for.
71
     * @param mixed|object $definition Definition to set.
72
     */
73
    public function set(string $id, $definition): void
74
    {
75
        $this->definitions[$id] = $definition;
76
    }
77
78 8
    private function isResolvable(string $id, array $building): bool
79
    {
80 8
        if (isset($this->definitions[$id])) {
81 1
            return true;
82
        }
83
84 7
        if (!class_exists($id)) {
85 4
            $this->buildStack += $building + [$id => 1];
86 4
            return false;
87
        }
88
89 5
        if (isset($building[$id])) {
90
            throw new CircularReferenceException(sprintf(
91
                'Circular reference to "%s" detected while building: %s.',
92
                $id,
93
                implode(', ', array_keys($building))
94
            ));
95
        }
96
97
        try {
98 5
            $dependencies = DefinitionExtractor::getInstance()->fromClassName($id);
99 2
        } catch (\Throwable $e) {
100 2
            $this->buildStack += $building + [$id => 1];
101 2
            return false;
102
        }
103
104 4
        if ($dependencies === []) {
105
            $this->definitions[$id] = $id;
106
            return true;
107
        }
108
109 4
        $isResolvable = true;
110 4
        $building[$id] = 1;
111
112
        try {
113 4
            foreach ($dependencies as $dependency) {
114 4
                $parameter = $dependency->getReflection();
115 4
                $type = $parameter->getType();
116
117 4
                if ($parameter->isVariadic() || $parameter->isOptional()) {
118
                    break;
119
                }
120
121
                /**
122
                 * @var ReflectionNamedType|ReflectionUnionType|null $type
123
                 * @psalm-suppress RedundantConditionGivenDocblockType
124
                 * @psalm-suppress UndefinedClass
125
                 */
126 4
                if ($type === null || !$type instanceof ReflectionUnionType && $type->isBuiltin()) {
127 1
                    $isResolvable = false;
128 1
                    break;
129
                }
130
131
                // PHP 8 union type is used as type hint
132
                /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
133 3
                if ($type instanceof ReflectionUnionType) {
134
                    $isUnionTypeResolvable = false;
135
                    $unionTypes = [];
136
                    /** @var ReflectionNamedType $unionType */
137
                    foreach ($type->getTypes() as $unionType) {
138
                        if (!$unionType->isBuiltin()) {
139
                            $typeName = $unionType->getName();
140
                            if ($typeName === 'self') {
141
                                continue;
142
                            }
143
                            $unionTypes[] = $typeName;
144
                            if ($this->isResolvable($typeName, $building)) {
145
                                $isUnionTypeResolvable = true;
146
                                break;
147
                            }
148
                        }
149
                    }
150
151
152
                    if (!$isUnionTypeResolvable) {
153
                        foreach ($unionTypes as $typeName) {
154
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
155
                                $isUnionTypeResolvable = true;
156
                                break;
157
                            }
158
                        }
159
160
                        $isResolvable = $isUnionTypeResolvable;
161
                        if (!$isResolvable) {
162
                            break;
163
                        }
164
                    }
165
                    continue;
166
                }
167
168
                /** @var ReflectionNamedType|null $type */
169
                // Our parameter has a class type hint
170 3
                if ($type !== null && !$type->isBuiltin()) {
171 3
                    $typeName = $type->getName();
172
                    /**
173
                     * @psalm-suppress TypeDoesNotContainType
174
                     *
175
                     * @link https://github.com/vimeo/psalm/issues/6756
176
                     */
177 3
                    if ($typeName === 'self') {
178
                        throw new CircularReferenceException(sprintf(
179
                            'Circular reference to "%s" detected while building: %s.',
180
                            $id,
181
                            implode(', ', array_keys($building))
182
                        ));
183
                    }
184
185
                    /** @psalm-suppress RedundantPropertyInitializationCheck */
186 3
                    if (!$this->isResolvable($typeName, $building) && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))) {
187 3
                        $isResolvable = false;
188 3
                        break;
189
                    }
190
                }
191
            }
192 4
        } finally {
193 4
            $this->buildStack += $building;
194 4
            unset($building[$id]);
195
        }
196
197 4
        if ($isResolvable) {
198
            $this->definitions[$id] = $id;
199
        }
200
201 4
        return $isResolvable;
202
    }
203
}
204