Passed
Pull Request — master (#25)
by Alexander
02:10
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 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
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
    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 10
    public function __construct(array $definitions = [], bool $useStrictMode = false)
31
    {
32 10
        $this->definitions = $definitions;
33 10
        $this->useStrictMode = $useStrictMode;
34 10
    }
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 10
    public function has(string $id): bool
52
    {
53 10
        $this->buildStack = [];
54 10
        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 2
    public function get(string $id)
74
    {
75 2
        if (!isset($this->definitions[$id])) {
76 1
            throw new RuntimeException("Service $id doesn't exist in DefinitionStorage.");
77
        }
78 1
        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
    public function set(string $id, $definition): void
88
    {
89
        $this->definitions[$id] = $definition;
90
    }
91
92 10
    private function isResolvable(string $id, array $building): bool
93
    {
94 10
        if (isset($this->definitions[$id])) {
95 1
            return true;
96
        }
97
98 9
        if ($this->useStrictMode) {
99 1
            return false;
100
        }
101
102 8
        if (!class_exists($id)) {
103 4
            $this->buildStack += $building + [$id => 1];
104 4
            return false;
105
        }
106
107 6
        if (isset($building[$id])) {
108
            throw new CircularReferenceException(sprintf(
109
                'Circular reference to "%s" detected while building: %s.',
110
                $id,
111
                implode(', ', array_keys($building))
112
            ));
113
        }
114
115
        try {
116 6
            $dependencies = DefinitionExtractor::fromClassName($id);
117 2
        } catch (Throwable $e) {
118 2
            $this->buildStack += $building + [$id => 1];
119 2
            return false;
120
        }
121
122 5
        if ($dependencies === []) {
123
            $this->definitions[$id] = $id;
124
            return true;
125
        }
126
127 5
        $isResolvable = true;
128 5
        $building[$id] = 1;
129
130
        try {
131 5
            foreach ($dependencies as $dependency) {
132 5
                $parameter = $dependency->getReflection();
133 5
                $type = $parameter->getType();
134
135 5
                if ($parameter->isVariadic() || $parameter->isOptional()) {
136 1
                    break;
137
                }
138
139
                /**
140
                 * @var ReflectionNamedType|ReflectionUnionType|null $type
141
                 * @psalm-suppress RedundantConditionGivenDocblockType
142
                 * @psalm-suppress UndefinedClass
143
                 */
144 4
                if ($type === null || (!$type instanceof ReflectionUnionType && $type->isBuiltin())) {
145 1
                    $isResolvable = false;
146 1
                    break;
147
                }
148
149
                // PHP 8 union type is used as type hint
150
                /** @psalm-suppress UndefinedClass, TypeDoesNotContainType */
151 3
                if ($type instanceof ReflectionUnionType) {
152
                    $isUnionTypeResolvable = false;
153
                    $unionTypes = [];
154
                    /** @var ReflectionNamedType $unionType */
155
                    foreach ($type->getTypes() as $unionType) {
156
                        if (!$unionType->isBuiltin()) {
157
                            $typeName = $unionType->getName();
158
                            if ($typeName === 'self') {
159
                                continue;
160
                            }
161
                            $unionTypes[] = $typeName;
162
                            if ($this->isResolvable($typeName, $building)) {
163
                                $isUnionTypeResolvable = true;
164
                                break;
165
                            }
166
                        }
167
                    }
168
169
170
                    if (!$isUnionTypeResolvable) {
171
                        foreach ($unionTypes as $typeName) {
172
                            if ($this->delegateContainer !== null && $this->delegateContainer->has($typeName)) {
173
                                $isUnionTypeResolvable = true;
174
                                break;
175
                            }
176
                        }
177
178
                        $isResolvable = $isUnionTypeResolvable;
179
                        if (!$isResolvable) {
180
                            break;
181
                        }
182
                    }
183
                    continue;
184
                }
185
186
                /** @var ReflectionNamedType|null $type */
187
                // Our parameter has a class type hint
188 3
                if ($type !== null && !$type->isBuiltin()) {
189 3
                    $typeName = $type->getName();
190
                    /**
191
                     * @psalm-suppress TypeDoesNotContainType
192
                     *
193
                     * @link https://github.com/vimeo/psalm/issues/6756
194
                     */
195 3
                    if ($typeName === 'self') {
196
                        throw new CircularReferenceException(sprintf(
197
                            'Circular reference to "%s" detected while building: %s.',
198
                            $id,
199
                            implode(', ', array_keys($building))
200
                        ));
201
                    }
202
203
                    /** @psalm-suppress RedundantPropertyInitializationCheck */
204 3
                    if (!$this->isResolvable($typeName, $building) && ($this->delegateContainer === null || !$this->delegateContainer->has($typeName))) {
205 3
                        $isResolvable = false;
206 3
                        break;
207
                    }
208
                }
209
            }
210 5
        } finally {
211 5
            $this->buildStack += $building;
212 5
            unset($building[$id]);
213
        }
214
215 5
        if ($isResolvable) {
216 1
            $this->definitions[$id] = $id;
217
        }
218
219 5
        return $isResolvable;
220
    }
221
}
222