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

DefinitionStorage   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Test Coverage

Coverage 87.88%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 97
c 3
b 1
f 0
dl 0
loc 236
ccs 87
cts 99
cp 0.8788
rs 9.0399
wmc 42

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getBuildStack() 0 3 1
A get() 0 6 2
A set() 0 3 1
A __construct() 0 4 1
A setDelegateContainer() 0 3 1
A has() 0 4 1
D isResolvable() 0 155 35

How to fix   Complexity   

Complex Class

Complex classes like DefinitionStorage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DefinitionStorage, and based on these observations, apply Extract Interface, too.

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