Passed
Push — master ( e0690f...bab3ce )
by Alexander
02:28
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 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 $building = [];
23
    /** @psalm-suppress  PropertyNotSetInConstructor */
24
    private ContainerInterface $delegateContainer;
25
26 97
    public function __construct(array $definitions = [])
27
    {
28 97
        $this->definitions = $definitions;
29 97
    }
30
31 90
    public function setDelegateContainer(ContainerInterface $delegateContainer): void
32
    {
33 90
        $this->delegateContainer = $delegateContainer;
34 90
    }
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 91
    public function has(string $id): bool
44
    {
45 91
        if (isset($this->definitions[$id])) {
46 91
            return true;
47
        }
48
49 57
        if (!class_exists($id)) {
50 14
            return false;
51
        }
52
53 52
        if (isset($this->building[$id])) {
54 4
            throw new CircularReferenceException(sprintf(
55 4
                'Circular reference to "%s" detected while building: %s.',
56
                $id,
57 4
                implode(', ', array_keys($this->building))
58
            ));
59
        }
60
61
        try {
62 52
            $reflectionClass = new ReflectionClass($id);
63
        } catch (ReflectionException $e) {
64
            return false;
65
        }
66
67 52
        if (!$reflectionClass->isInstantiable()) {
68
            return false;
69
        }
70
71 52
        $constructor = $reflectionClass->getConstructor();
72
73 52
        if ($constructor === null) {
74 5
            $this->definitions[$id] = $id;
75 5
            return true;
76
        }
77
78 50
        $isResolvable = true;
79 50
        $this->building[$id] = 1;
80
81
        try {
82 50
            foreach ($constructor->getParameters() as $parameter) {
83 50
                $type = $parameter->getType();
84
85 50
                if ($parameter->isVariadic() || $parameter->isOptional()) {
86 44
                    break;
87
                }
88
89
                /**
90
                 * @var ReflectionNamedType|ReflectionUnionType|null $type
91
                 * @psalm-suppress RedundantConditionGivenDocblockType
92
                 * @psalm-suppress UndefinedClass
93
                 */
94 20
                if ($type === null || !$type instanceof ReflectionUnionType && $type->isBuiltin()) {
95 4
                    $isResolvable = false;
96 4
                    break;
97
                }
98
99
                // PHP 8 union type is used as type hint
100
                /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
101 20
                if ($type instanceof ReflectionUnionType) {
102
                    $isUnionTypeResolvable = false;
103
                    $unionTypes = [];
104
                    /** @var ReflectionNamedType $unionType */
105
                    foreach ($type->getTypes() as $unionType) {
106
                        if (!$unionType->isBuiltin()) {
107
                            $typeName = $unionType->getName();
108
                            if ($typeName === 'self') {
109
                                continue;
110
                            }
111
                            $unionTypes[] = $typeName;
112
                            if ($this->has($typeName)) {
113
                                $isUnionTypeResolvable = true;
114
                                break;
115
                            }
116
                        }
117
                    }
118
119
120
                    if (!$isUnionTypeResolvable) {
121
                        foreach ($unionTypes as $typeName) {
122
                            if ($this->delegateContainer->has($typeName)) {
123
                                $isUnionTypeResolvable = true;
124
                                break;
125
                            }
126
                        }
127
128
                        $isResolvable = $isUnionTypeResolvable;
129
                        if (!$isResolvable) {
130
                            break;
131
                        }
132
                    }
133
                    continue;
134
                }
135
136
                /** @var ReflectionNamedType|null $type */
137
                // Our parameter has a class type hint
138 20
                if ($type !== null && !$type->isBuiltin()) {
139 20
                    $typeName = $type->getName();
140
141 20
                    if ($typeName === 'self') {
142 1
                        throw new CircularReferenceException(sprintf(
143 1
                            'Circular reference to "%s" detected while building: %s.',
144
                            $id,
145 1
                            implode(', ', array_keys($this->building))
146
                        ));
147
                    }
148
149
                    /** @psalm-suppress RedundantPropertyInitializationCheck */
150 20
                    if (!($this->has($typeName) || (isset($this->delegateContainer) ? $this->delegateContainer->has($typeName) : false))) {
151 7
                        $isResolvable = false;
152 7
                        break;
153
                    }
154
                }
155
            }
156 47
        } finally {
157 50
            unset($this->building[$id]);
158
        }
159
160 47
        if ($isResolvable) {
161 44
            $this->definitions[$id] = $id;
162
        }
163
164 47
        return $isResolvable;
165
    }
166
167
    /**
168
     * Get a definition with a given ID.
169
     *
170
     * @return mixed|object Definition with a given ID.
171
     */
172 91
    public function get(string $id)
173
    {
174 91
        if (!isset($this->definitions[$id])) {
175
            throw new \RuntimeException("Service $id doesn't exist in DefinitionStorage.");
176
        }
177 91
        return $this->definitions[$id];
178
    }
179
180
    /**
181
     * Set a definition.
182
     *
183
     * @param string $id ID to set definition for.
184
     * @param mixed|object $definition Definition to set.
185
     */
186 97
    public function set(string $id, $definition): void
187
    {
188 97
        $this->definitions[$id] = $definition;
189 97
    }
190
}
191