DefinitionStorage   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 217
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 82
c 1
b 0
f 0
dl 0
loc 217
ccs 85
cts 85
cp 1
rs 9.36
wmc 38

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getBuildStack() 0 3 1
A get() 0 6 2
A set() 0 3 1
A __construct() 0 4 1
A setDelegateContainer() 0 3 1
A has() 0 4 1
D isResolvable() 0 136 31
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Definitions;
6
7
use Psr\Container\ContainerInterface;
8
use ReflectionNamedType;
9
use ReflectionUnionType;
10
use RuntimeException;
11
use Throwable;
12
use Yiisoft\Definitions\Exception\CircularReferenceException;
13
use Yiisoft\Definitions\Helpers\DefinitionExtractor;
14
15
/**
16
 * Stores service definitions and checks if a definition could be instantiated.
17
 */
18
final class DefinitionStorage
19
{
20
    /**
21
     * @var array<string,1>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string,1> at position 4 could not be parsed: Unknown type name '1' at position 4 in array<string,1>.
Loading history...
22
     */
23
    private array $buildStack = [];
24
25
    private ?ContainerInterface $delegateContainer = null;
26
27
    /**
28
     * @param array $definitions Definitions to store.
29
     * @param bool $useStrictMode If every dependency should be defined explicitly including classes.
30
     */
31 25
    public function __construct(
32
        private array $definitions = [],
33
        private bool $useStrictMode = false
34
    ) {
35 25
    }
36
37
    /**
38
     * @param ContainerInterface $delegateContainer Container to fall back to when dependency is not found.
39
     */
40 5
    public function setDelegateContainer(ContainerInterface $delegateContainer): void
41
    {
42 5
        $this->delegateContainer = $delegateContainer;
43
    }
44
45
    /**
46
     * Checks if there is a definition with ID specified and that it can be created.
47
     *
48
     * @param string $id class name, interface name or alias name
49
     *
50
     * @throws CircularReferenceException
51
     */
52 25
    public function has(string $id): bool
53
    {
54 25
        $this->buildStack = [];
55 25
        return $this->isResolvable($id, []);
56
    }
57
58
    /**
59
     * Returns a stack with definition IDs in the order the latest dependency obtained would be built.
60
     *
61
     * @return string[] Build stack.
62
     */
63 11
    public function getBuildStack(): array
64
    {
65 11
        return array_keys($this->buildStack);
66
    }
67
68
    /**
69
     * Get a definition with a given ID.
70
     *
71
     * @throws CircularReferenceException
72
     *
73
     * @return mixed|object Definition with a given ID.
74
     */
75 10
    public function get(string $id): mixed
76
    {
77 10
        if (!$this->has($id)) {
78 2
            throw new RuntimeException("Service $id doesn't exist in DefinitionStorage.");
79
        }
80 5
        return $this->definitions[$id];
81
    }
82
83
    /**
84
     * Set a definition.
85
     *
86
     * @param string $id ID to set definition for.
87
     * @param mixed $definition Definition to set.
88
     */
89 1
    public function set(string $id, mixed $definition): void
90
    {
91 1
        $this->definitions[$id] = $definition;
92
    }
93
94
    /**
95
     * @param array<string,1> $building
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string,1> at position 4 could not be parsed: Unknown type name '1' at position 4 in array<string,1>.
Loading history...
96
     *
97
     * @throws CircularReferenceException
98
     */
99 25
    private function isResolvable(string $id, array $building): bool
100
    {
101 25
        if (isset($this->definitions[$id])) {
102 4
            return true;
103
        }
104
105 23
        if ($this->useStrictMode || !class_exists($id)) {
106 13
            $this->buildStack += $building + [$id => 1];
107 13
            return false;
108
        }
109
110 20
        if (isset($building[$id])) {
111 2
            throw new CircularReferenceException(sprintf(
112 2
                'Circular reference to "%s" detected while building: %s.',
113 2
                $id,
114 2
                implode(', ', array_keys($building))
115 2
            ));
116
        }
117
118
        try {
119 20
            $dependencies = DefinitionExtractor::fromClassName($id);
120 3
        } catch (Throwable) {
121 3
            $this->buildStack += $building + [$id => 1];
122 3
            return false;
123
        }
124
125 19
        if ($dependencies === []) {
126 1
            $this->definitions[$id] = $id;
127 1
            return true;
128
        }
129
130 18
        $isResolvable = true;
131 18
        $building[$id] = 1;
132
133
        try {
134 18
            foreach ($dependencies as $dependency) {
135 18
                $parameter = $dependency->getReflection();
136 18
                $type = $parameter->getType();
137
138 18
                if ($parameter->isVariadic() || $parameter->isOptional()) {
139
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
140 3
                    break;
141
                }
142
143
                if (
144 17
                    ($type instanceof ReflectionNamedType && $type->isBuiltin())
145 17
                    || (!$type instanceof ReflectionNamedType && !$type instanceof ReflectionUnionType)
146
                ) {
147 2
                    $isResolvable = false;
148 2
                    break;
149
                }
150
151
                /** @var ReflectionNamedType|ReflectionUnionType $type */
152
153
                // Union type is used as type hint
154 15
                if ($type instanceof ReflectionUnionType) {
155 7
                    $isUnionTypeResolvable = false;
156 7
                    $unionTypes = [];
157 7
                    foreach ($type->getTypes() as $unionType) {
158
                        /**
159
                         * @psalm-suppress DocblockTypeContradiction Need for PHP 8.0 and 8.1 only
160
                         */
161 7
                        if (!$unionType instanceof ReflectionNamedType || $unionType->isBuiltin()) {
162 1
                            continue;
163
                        }
164
165 7
                        $typeName = $unionType->getName();
166
                        /**
167
                         * @psalm-suppress TypeDoesNotContainType
168
                         *
169
                         * @link https://github.com/vimeo/psalm/issues/6756
170
                         */
171 7
                        if ($typeName === 'self') {
172 2
                            continue;
173
                        }
174 7
                        $unionTypes[] = $typeName;
175 7
                        if ($this->isResolvable($typeName, $building)) {
176 2
                            $isUnionTypeResolvable = true;
177
                            /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
178 2
                            break;
179
                        }
180
                    }
181
182 7
                    if (!$isUnionTypeResolvable) {
183 5
                        foreach ($unionTypes as $typeName) {
184 5
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
185 1
                                $isUnionTypeResolvable = true;
186
                                /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
187 1
                                break;
188
                            }
189
                        }
190
191 5
                        $isResolvable = $isUnionTypeResolvable;
192 5
                        if (!$isResolvable) {
193 4
                            break;
194
                        }
195
                    }
196 3
                    continue;
197
                }
198
199
                // Our parameter has a class type hint
200 10
                if (!$type->isBuiltin()) {
201 10
                    $typeName = $type->getName();
202
                    /**
203
                     * @psalm-suppress TypeDoesNotContainType
204
                     *
205
                     * @link https://github.com/vimeo/psalm/issues/6756
206
                     */
207 10
                    if ($typeName === 'self') {
208 1
                        throw new CircularReferenceException(
209 1
                            sprintf(
210 1
                                'Circular reference to "%s" detected while building: %s.',
211 1
                                $id,
212 1
                                implode(', ', array_keys($building))
213 1
                            )
214 1
                        );
215
                    }
216
217
                    if (
218 9
                        !$this->isResolvable($typeName, $building)
219 7
                        && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))
220
                    ) {
221 6
                        $isResolvable = false;
222 6
                        break;
223
                    }
224
                }
225
            }
226
        } finally {
227 18
            $this->buildStack += $building;
228
        }
229
230 15
        if ($isResolvable) {
231 4
            $this->definitions[$id] = $id;
232
        }
233
234 15
        return $isResolvable;
235
    }
236
}
237