Passed
Push — master ( 8afadc...1cb41c )
by Alexander
03:09 queued 54s
created

DefinitionStorage::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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