Passed
Pull Request — master (#87)
by Dmitriy
12:15
created

DefinitionStorage::isResolvable()   D

Complexity

Conditions 35
Paths 81

Size

Total Lines 155
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 72
CRAP Score 38.5745

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 35
eloc 86
c 3
b 1
f 0
nc 81
nop 3
dl 0
loc 155
ccs 72
cts 84
cp 0.8571
crap 38.5745
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 7
                        if (!$unionType->isBuiltin()) {
183 7
                            $typeName = $unionType->getName();
184
                            /**
185
                             * @psalm-suppress TypeDoesNotContainType
186
                             *
187
                             * @link https://github.com/vimeo/psalm/issues/6756
188
                             */
189 7
                            if ($typeName === 'self') {
190 2
                                continue;
191
                            }
192 7
                            $unionTypes[] = $typeName;
193 7
                            if ($this->isResolvable($typeName, $building, $parameter->getName())) {
194 2
                                $isUnionTypeResolvable = true;
195
                                /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
196 2
                                break;
197
                            }
198
                        }
199
                    }
200
201 7
                    if (!$isUnionTypeResolvable) {
202 5
                        foreach ($unionTypes as $typeName) {
203 5
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
204 1
                                $isUnionTypeResolvable = true;
205
                                /** @infection-ignore-all Mutation don't change behaviour, but degrade performance. */
206 1
                                break;
207
                            }
208
                        }
209
210 5
                        $isResolvable = $isUnionTypeResolvable;
211 5
                        if (!$isResolvable) {
212 4
                            break;
213
                        }
214
                    }
215 3
                    continue;
216
                }
217
218
                // Our parameter has a class type hint
219 10
                if (!$type->isBuiltin()) {
220 10
                    $typeName = $type->getName();
221
                    /**
222
                     * @psalm-suppress TypeDoesNotContainType
223
                     *
224
                     * @link https://github.com/vimeo/psalm/issues/6756
225
                     */
226 10
                    if ($typeName === 'self') {
227 1
                        throw new CircularReferenceException(
228 1
                            sprintf(
229 1
                                'Circular reference to "%s" detected while building: %s.',
230 1
                                $id,
231 1
                                implode(', ', array_keys($building))
232 1
                            )
233 1
                        );
234
                    }
235
236
                    if (
237 9
                        !$this->isResolvable($typeName, $building, $parameter->getName())
238 7
                        && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))
239
                    ) {
240 6
                        $isResolvable = false;
241 6
                        break;
242
                    }
243
                }
244
            }
245
        } finally {
246 18
            $this->buildStack += $building;
247
        }
248
249 15
        if ($isResolvable && !isset($this->definitions[$id])) {
250 4
            $this->definitions[$id] = $id;
251
        }
252
253 15
        return $isResolvable;
254
    }
255
}
256