Passed
Pull Request — master (#87)
by Dmitriy
07:48 queued 05:22
created

DefinitionStorage::setDelegateContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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