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

DefinitionStorage::isResolvable()   D

Complexity

Conditions 36
Paths 71

Size

Total Lines 160
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 73
CRAP Score 39.6484

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 36
eloc 87
c 3
b 1
f 0
nc 71
nop 3
dl 0
loc 160
ccs 73
cts 85
cp 0.8588
crap 39.6484
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\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