Passed
Pull Request — master (#248)
by Dmitriy
02:20
created

DefinitionStorage::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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