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

DefinitionStorage   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 198
Duplicated Lines 0 %

Test Coverage

Coverage 75.61%

Importance

Changes 0
Metric Value
eloc 81
c 0
b 0
f 0
dl 0
loc 198
ccs 62
cts 82
cp 0.7561
rs 9.44
wmc 37

7 Methods

Rating   Name   Duplication   Size   Complexity  
A get() 0 6 2
A __construct() 0 4 1
A setDelegateContainer() 0 3 1
A getBuildStack() 0 3 1
A has() 0 4 1
A set() 0 3 1
D isResolvable() 0 124 30
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