Passed
Push — master ( 76d43e...ace620 )
by Alexander
02:16
created

DefinitionStorage::isResolvable()   D

Complexity

Conditions 30
Paths 79

Size

Total Lines 124
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 58.8053

Importance

Changes 0
Metric Value
cc 30
eloc 66
c 0
b 0
f 0
nc 79
nop 2
dl 0
loc 124
ccs 43
cts 63
cp 0.6825
crap 58.8053
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
    private array $definitions;
21
    private array $buildStack = [];
22
    /** @psalm-suppress  PropertyNotSetInConstructor */
23
    private ?ContainerInterface $delegateContainer = null;
24
    private bool $useStrictMode;
25
26
    /**
27
     * @param array $definitions Definitions to store.
28
     * @param bool $useStrictMode If every dependency should be defined explicitly including classes.
29
     */
30 14
    public function __construct(array $definitions = [], bool $useStrictMode = false)
31
    {
32 14
        $this->definitions = $definitions;
33 14
        $this->useStrictMode = $useStrictMode;
34 14
    }
35
36
    /**
37
     * @param ContainerInterface $delegateContainer Container to fall back to when dependency is not found.
38
     */
39 1
    public function setDelegateContainer(ContainerInterface $delegateContainer): void
40
    {
41 1
        $this->delegateContainer = $delegateContainer;
42 1
    }
43
44
    /**
45
     * Checks if there is a definition with ID specified and that it can be created.
46
     *
47
     * @param string $id class name, interface name or alias name
48
     *
49
     * @throws CircularReferenceException
50
     */
51 14
    public function has(string $id): bool
52
    {
53 14
        $this->buildStack = [];
54 14
        return $this->isResolvable($id, []);
55
    }
56
57
    /**
58
     * Returns a stack with definition IDs as keys and 1 as values in the order the latest dependency obtained would
59
     * be built.
60
     *
61
     * @return array Build stack.
62
     */
63 8
    public function getBuildStack(): array
64
    {
65 8
        return $this->buildStack;
66
    }
67
68
    /**
69
     * Get a definition with a given ID.
70
     *
71
     * @return mixed|object Definition with a given ID.
72
     */
73 6
    public function get(string $id)
74
    {
75 6
        if (!$this->has($id)) {
76 1
            throw new RuntimeException("Service $id doesn't exist in DefinitionStorage.");
77
        }
78 3
        return $this->definitions[$id];
79
    }
80
81
    /**
82
     * Set a definition.
83
     *
84
     * @param string $id ID to set definition for.
85
     * @param mixed|object $definition Definition to set.
86
     */
87 1
    public function set(string $id, $definition): void
88
    {
89 1
        $this->definitions[$id] = $definition;
90 1
    }
91
92 14
    private function isResolvable(string $id, array $building): bool
93
    {
94 14
        if (isset($this->definitions[$id])) {
95 2
            return true;
96
        }
97
98 12
        if ($this->useStrictMode || !class_exists($id)) {
99 5
            $this->buildStack += $building + [$id => 1];
100 5
            return false;
101
        }
102
103 9
        if (isset($building[$id])) {
104 1
            throw new CircularReferenceException(sprintf(
105 1
                'Circular reference to "%s" detected while building: %s.',
106
                $id,
107 1
                implode(', ', array_keys($building))
108
            ));
109
        }
110
111
        try {
112 9
            $dependencies = DefinitionExtractor::fromClassName($id);
113 2
        } catch (Throwable $e) {
114 2
            $this->buildStack += $building + [$id => 1];
115 2
            return false;
116
        }
117
118 8
        if ($dependencies === []) {
119 1
            $this->definitions[$id] = $id;
120 1
            return true;
121
        }
122
123 7
        $isResolvable = true;
124 7
        $building[$id] = 1;
125
126
        try {
127 7
            foreach ($dependencies as $dependency) {
128 7
                $parameter = $dependency->getReflection();
129 7
                $type = $parameter->getType();
130
131 7
                if ($parameter->isVariadic() || $parameter->isOptional()) {
132 1
                    break;
133
                }
134
135
                /**
136
                 * @var ReflectionNamedType|ReflectionUnionType|null $type
137
                 * @psalm-suppress RedundantConditionGivenDocblockType
138
                 * @psalm-suppress UndefinedClass
139
                 */
140 6
                if ($type === null || (!$type instanceof ReflectionUnionType && $type->isBuiltin())) {
141 1
                    $isResolvable = false;
142 1
                    break;
143
                }
144
145
                // PHP 8 union type is used as type hint
146
                /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
147 5
                if ($type instanceof ReflectionUnionType) {
148
                    $isUnionTypeResolvable = false;
149
                    $unionTypes = [];
150
                    /** @var ReflectionNamedType $unionType */
151
                    foreach ($type->getTypes() as $unionType) {
152
                        if (!$unionType->isBuiltin()) {
153
                            $typeName = $unionType->getName();
154
                            if ($typeName === 'self') {
155
                                continue;
156
                            }
157
                            $unionTypes[] = $typeName;
158
                            if ($this->isResolvable($typeName, $building)) {
159
                                $isUnionTypeResolvable = true;
160
                                break;
161
                            }
162
                        }
163
                    }
164
165
166
                    if (!$isUnionTypeResolvable) {
167
                        foreach ($unionTypes as $typeName) {
168
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
169
                                $isUnionTypeResolvable = true;
170
                                break;
171
                            }
172
                        }
173
174
                        $isResolvable = $isUnionTypeResolvable;
175
                        if (!$isResolvable) {
176
                            break;
177
                        }
178
                    }
179
                    continue;
180
                }
181
182
                /** @var ReflectionNamedType|null $type */
183
                // Our parameter has a class type hint
184 5
                if ($type !== null && !$type->isBuiltin()) {
185 5
                    $typeName = $type->getName();
186
                    /**
187
                     * @psalm-suppress TypeDoesNotContainType
188
                     *
189
                     * @link https://github.com/vimeo/psalm/issues/6756
190
                     */
191 5
                    if ($typeName === 'self') {
192 1
                        throw new CircularReferenceException(sprintf(
193 1
                            'Circular reference to "%s" detected while building: %s.',
194
                            $id,
195 1
                            implode(', ', array_keys($building))
196
                        ));
197
                    }
198
199
                    /** @psalm-suppress RedundantPropertyInitializationCheck */
200 4
                    if (!$this->isResolvable($typeName, $building) && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))) {
201 3
                        $isResolvable = false;
202 3
                        break;
203
                    }
204
                }
205
            }
206 5
        } finally {
207 7
            $this->buildStack += $building;
208 7
            unset($building[$id]);
209
        }
210
211 5
        if ($isResolvable) {
212 1
            $this->definitions[$id] = $id;
213
        }
214
215 5
        return $isResolvable;
216
    }
217
}
218