Passed
Pull Request — master (#87)
by Dmitriy
12:25 queued 49s
created

DefinitionStorage::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
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, ?string $parameterName = null): bool
100
    {
101 25
        if (isset($this->definitions[$id])) {
102 4
            return true;
103
        }
104
105
        if (
106 23
            $parameterName !== null
107
            && (
108 23
                isset($this->definitions[$typedParameterName = $id . ' $' . $parameterName])
109 23
                || isset($this->definitions[$typedParameterName = '$' . $parameterName])
110
            )
111
        ) {
112
            $buildingClass = array_key_last($building);
113
            $definition = $this->definitions[$buildingClass] ?? null;
114
            $temporaryDefinition = ArrayDefinition::fromConfig([
115
                ArrayDefinition::CLASS_NAME => $buildingClass,
116
                ArrayDefinition::CONSTRUCTOR => [
117
                    $parameterName => Reference::to($this->definitions[$typedParameterName]),
118
                ],
119
            ]);
120
            if ($definition instanceof ArrayDefinition) {
121
                $this->definitions[$buildingClass] = $definition->merge($temporaryDefinition);
122
            } else {
123
                $this->definitions[$buildingClass] = $temporaryDefinition;
124
            }
125
126
            return true;
127
        }
128
129 23
        if ($this->useStrictMode || !class_exists($id)) {
130 13
            $this->buildStack += $building + [$id => 1];
131 13
            return false;
132
        }
133
134 20
        if (isset($building[$id])) {
135 2
            throw new CircularReferenceException(sprintf(
136 2
                'Circular reference to "%s" detected while building: %s.',
137 2
                $id,
138 2
                implode(', ', array_keys($building))
139 2
            ));
140
        }
141
142
        try {
143 20
            $dependencies = DefinitionExtractor::fromClassName($id);
144 3
        } catch (Throwable) {
145 3
            $this->buildStack += $building + [$id => 1];
146 3
            return false;
147
        }
148
149 19
        if ($dependencies === []) {
150 1
            $this->definitions[$id] = $id;
151 1
            return true;
152
        }
153
154 18
        $isResolvable = true;
155 18
        $building[$id] = 1;
156
157
        try {
158 18
            foreach ($dependencies as $dependency) {
159 18
                $parameter = $dependency->getReflection();
160 18
                $type = $parameter->getType();
161
162 18
                if ($parameter->isVariadic() || $parameter->isOptional()) {
163
                    /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
164 3
                    break;
165
                }
166
167
                if (
168 17
                    ($type instanceof ReflectionNamedType && $type->isBuiltin())
169 17
                    || (!$type instanceof ReflectionNamedType && !$type instanceof ReflectionUnionType)
170
                ) {
171 2
                    $isResolvable = false;
172 2
                    break;
173
                }
174
175
                /** @var ReflectionNamedType|ReflectionUnionType $type */
176
177
                // Union type is used as type hint
178 15
                if ($type instanceof ReflectionUnionType) {
179 7
                    $isUnionTypeResolvable = false;
180 7
                    $unionTypes = [];
181 7
                    foreach ($type->getTypes() as $unionType) {
182
                        /**
183
                         * @psalm-suppress DocblockTypeContradiction Need for PHP 8.0 and 8.1 only
184
                         */
185 7
                        if (!$unionType instanceof ReflectionNamedType || $unionType->isBuiltin()) {
186 1
                            continue;
187
                        }
188
189 7
                        $typeName = $unionType->getName();
190
                        /**
191
                         * @psalm-suppress TypeDoesNotContainType
192
                         *
193
                         * @link https://github.com/vimeo/psalm/issues/6756
194
                         */
195 7
                        if ($typeName === 'self') {
196 2
                            continue;
197
                        }
198 7
                        $unionTypes[] = $typeName;
199 7
                        if ($this->isResolvable($typeName, $building, $parameter->getName())) {
200 2
                            $isUnionTypeResolvable = true;
201
                            /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
202 2
                            break;
203
                        }
204
                    }
205
206 7
                    if (!$isUnionTypeResolvable) {
207 5
                        foreach ($unionTypes as $typeName) {
208 5
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
209 1
                                $isUnionTypeResolvable = true;
210
                                /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
211 1
                                break;
212
                            }
213
                        }
214
215 5
                        $isResolvable = $isUnionTypeResolvable;
216 5
                        if (!$isResolvable) {
217 4
                            break;
218
                        }
219
                    }
220 3
                    continue;
221
                }
222
223
                // Our parameter has a class type hint
224 10
                if (!$type->isBuiltin()) {
225 10
                    $typeName = $type->getName();
226
                    /**
227
                     * @psalm-suppress TypeDoesNotContainType
228
                     *
229
                     * @link https://github.com/vimeo/psalm/issues/6756
230
                     */
231 10
                    if ($typeName === 'self') {
232 1
                        throw new CircularReferenceException(
233 1
                            sprintf(
234 1
                                'Circular reference to "%s" detected while building: %s.',
235 1
                                $id,
236 1
                                implode(', ', array_keys($building))
237 1
                            )
238 1
                        );
239
                    }
240
241
                    if (
242 9
                        !$this->isResolvable($typeName, $building, $parameter->getName())
243 7
                        && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))
244
                    ) {
245 6
                        $isResolvable = false;
246 6
                        break;
247
                    }
248
                }
249
            }
250
        } finally {
251 18
            $this->buildStack += $building;
252
        }
253
254 15
        if ($isResolvable && !isset($this->definitions[$id])) {
255 4
            $this->definitions[$id] = $id;
256
        }
257
258 15
        return $isResolvable;
259
    }
260
}
261