Passed
Push — master ( 953b3a...731eeb )
by Alexander
02:48
created

DefinitionStorage::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

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