Passed
Pull Request — master (#87)
by Dmitriy
06:07 queued 03:11
created

DefinitionStorage::isResolvable()   D

Complexity

Conditions 34
Paths 81

Size

Total Lines 152
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 71
CRAP Score 37.4951

Importance

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