Passed
Push — modular-container ( c3741a )
by Viktor
03:13
created

ModuleRootContainer::getModuleContainer()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 27
c 1
b 0
f 0
nc 15
nop 1
dl 0
loc 43
ccs 0
cts 25
cp 0
crap 56
rs 8.5546
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Di;
6
7
use InvalidArgumentException;
8
use Psr\Container\ContainerInterface;
9
use RuntimeException;
10
use Yiisoft\Factory\Exception\NotFoundException;
11
12
final class ModuleRootContainer implements ContainerInterface
13
{
14
    private const KEY_DEFINITIONS = '#definitions';
15
    private const KEY_CONTAINER = '#container';
16
    private const KEY_SUBMODULE_NAMES = '#submoduleNames';
17
    public const KEY_SUBMODULES = '#submodules';
18
19
    private array $building = [];
20
    private array $definitionsDefault = [];
21
    private array $definitions = [];
22
    private array $definitionsPlain = [];
23
24
    /**
25
     * @param array $definitions
26
     * definition structure:
27
     * namespace => [
28
     *     dependencies => [namespace1, namespace2, namespace3]
29
     *     definitions => [classname => definition, ...]
30
     * ]
31
     */
32
    public function __construct(array $definitions)
33
    {
34
        $definitions = $this->prepareDefinitions($definitions);
35
        foreach ($definitions as $namespace => $moduleConfig) {
36
            $this->buildDefinitions($namespace, $definitions);
37
        }
38
    }
39
40
    private function setDefinitions(string $namespace, array $definitions): void
41
    {
42
        $definitionBag = &$this->definitions;
43
        foreach (explode('\\', trim($namespace, '\\')) as $part) {
44
            if (!isset($definitionBag[$part])) {
45
                $definitionBag[$part] = [];
46
            }
47
48
            $definitionBag = &$definitionBag[$part];
49
        }
50
51
        $definitionBag[self::KEY_DEFINITIONS] = $definitions;
52
        $definitionBag[self::KEY_CONTAINER] = null;
53
        $this->definitionsPlain[$namespace] = &$definitionBag[self::KEY_DEFINITIONS];
54
    }
55
56
    /**
57
     * @param string $id
58
     *
59
     * @return mixed
60
     */
61
    public function get($id)
62
    {
63
        return $this->resolved[$id] ?? $this->resolve($id);
0 ignored issues
show
Bug Best Practice introduced by
The property resolved does not exist on Yiisoft\Di\ModuleRootContainer. Did you maybe forget to declare it?
Loading history...
64
    }
65
66
    private function getModuleContainer(string $id): ?ContainerInterface
67
    {
68
        $resultBag = null;
69
        $resultNamespace = $tempNamespace = [];
70
        $definitionBag = &$this->definitions;
71
72
        $namespace = explode('\\', trim($id, '\\'));
73
        array_pop($namespace); // remove class name
74
75
        foreach ($namespace as $part) {
76
            $tempNamespace[] = $part;
77
            if (isset($definitionBag[$part][self::KEY_DEFINITIONS])) {
78
                $resultBag = &$definitionBag[$part];
79
                $resultNamespace = $tempNamespace;
80
            }
81
82
            if (isset($definitionBag[$part])) {
83
                $definitionBag = &$definitionBag[$part];
84
            } else {
85
                break;
86
            }
87
        }
88
89
        if ($resultBag === null) {
90
            return null;
91
        }
92
93
        if ($resultBag[self::KEY_CONTAINER] === null) {
94
            $submodules = $resultBag[self::KEY_DEFINITIONS][self::KEY_SUBMODULE_NAMES] ?? [];
95
            unset($resultBag[self::KEY_DEFINITIONS][self::KEY_SUBMODULE_NAMES]);
96
97
            $submoduleDefinitions = [];
98
            foreach ($submodules as $submodule) {
99
                $submoduleDefinitions[$submodule] = $this->buildSubmoduleTree($submodule);
100
            }
101
            $resultBag[self::KEY_CONTAINER] = new ModuleContainer(
102
                implode('\\', $resultNamespace),
103
                $resultBag[self::KEY_DEFINITIONS],
104
                $submoduleDefinitions
105
            );
106
        }
107
108
        return $resultBag[self::KEY_CONTAINER];
109
    }
110
111
    private function getDefinitionDefaultContainer(string $id): ?ContainerInterface
112
    {
113
        if (isset($this->definitionsDefault[$id])) {
114
            return $this->getModuleContainer($this->definitionsDefault[$id] . '\\Dummy');
115
        }
116
117
        return null;
118
    }
119
120
    private function prepareDefinitions(array $definitions): array
121
    {
122
        $result = [];
123
        foreach ($definitions as $namespace => $moduleConfig) {
124
            $result[trim($namespace, " \t\n\r\0\x0B\\")] = $moduleConfig;
125
        }
126
127
        return $result;
128
    }
129
130
    private function getNamespaceMatch(string $id, string $namespace): int
131
    {
132
        $idNamespace = explode('\\', trim($id, '\\'));
133
        array_pop($idNamespace); // remove class name
134
135
        $namespaceDivided = explode('\\', $namespace);
136
137
        $result = 0;
138
        foreach ($namespaceDivided as $i => $part) {
139
            if ($idNamespace[$i] === $part) {
140
                $result++;
141
            } else {
142
                return $result;
143
            }
144
        }
145
146
        return $result;
147
    }
148
149
    private function setDefaultDefinitions(string $namespace, array $definitions): void
150
    {
151
        foreach ($definitions as $id => $definition) {
152
            if (isset($this->definitionsDefault[$id])) {
153
                if (!class_exists($id) && !interface_exists($id)) {
154
                    $message = "Container definition id conflict: id '$id' exists "
155
                        . "in modules '$namespace' and '{$this->definitionsDefault[$id]}'";
156
157
                    throw new RuntimeException($message);
158
                }
159
160
                $matchCurrent = $this->getNamespaceMatch($id, $this->definitionsDefault[$id]);
161
                $matchNew = $this->getNamespaceMatch($id, $namespace);
162
                if ($matchNew !== 0 && $matchNew < $matchCurrent) {
163
                    $this->definitionsDefault[$id] = $namespace;
164
                }
165
            } else {
166
                $this->definitionsDefault[$id] = $namespace;
167
            }
168
        }
169
    }
170
171
    /**
172
     * Filters out current module definitions and remains only 3rd-party
173
     *
174
     * @param string $namespace
175
     * @param array $module
176
     *
177
     * @return array
178
     */
179
    private function getDependencyDefinitions(string $namespace, array $module): array
180
    {
181
        return array_filter(
182
            $module['definitions'] ?? [],
183
            fn(string $id) => $this->getNamespaceMatch($id, $namespace) === 0,
184
            ARRAY_FILTER_USE_KEY
185
        );
186
    }
187
188
    private function buildDefinitions(string $namespace, array $definitions): array
189
    {
190
        if (isset($this->definitionsPlain[$namespace])) {
191
            return $this->definitionsPlain[$namespace];
192
        }
193
194
        if (isset($this->building[$namespace])) {
195
            throw new RuntimeException('Circular module dependency');
196
        }
197
198
        $this->building[$namespace] = true;
199
        $moduleConfig = $definitions[$namespace];
200
201
        $this->setDefaultDefinitions($namespace, $moduleConfig['definitions'] ?? []);
202
        $definitionParts = [$moduleConfig['definitions'] ?? []];
203
        foreach ($moduleConfig['dependencies'] ?? [] as $dependencyNamespace) {
204
            if (!isset($definitions[$dependencyNamespace])) {
205
                throw new InvalidArgumentException(
206
                    "Dependency '$dependencyNamespace' of module '$namespace' is not defined"
207
                );
208
            }
209
210
            if (strpos($dependencyNamespace, $namespace) === 0) {
211
                // Dependency is a submodule of the current module
212
                $definitionParts[0][self::KEY_SUBMODULE_NAMES][] = $dependencyNamespace;
213
            } elseif (strpos($namespace, $dependencyNamespace) === 0) {
214
                // Dependency is a parent of the current module
215
                $parentDefinitions = $this->buildDefinitions($dependencyNamespace, $definitions);
216
217
                $definitionParts[] = $this->getDependencyDefinitions($dependencyNamespace, $parentDefinitions);
218
                foreach ($parentDefinitions[self::KEY_SUBMODULE_NAMES] as $parentSubmodule) {
219
                    if (strpos($parentSubmodule, $dependencyNamespace) !== 0) {
220
                        $definitionParts[0][self::KEY_SUBMODULE_NAMES][] = $parentSubmodule;
221
                    }
222
                }
223
            } else {
224
                // 3rd-party dependency
225
                $dependencyDefinitions = $this->buildDefinitions($dependencyNamespace, $definitions);
226
227
                $definitionParts[] = $dependencyDefinitions;
228
                $definitionParts[0][self::KEY_SUBMODULE_NAMES][] = $dependencyNamespace;
229
            }
230
        }
231
232
        $moduleDefinitions = array_merge(...array_reverse($definitionParts));
233
        $this->setDefinitions($namespace, $moduleDefinitions);
234
235
        unset($this->building[$namespace]);
236
237
        return $moduleDefinitions;
238
    }
239
240
    private function resolve(string $id)
241
    {
242
        if (class_exists($id)) {
243
            $container = $this->getModuleContainer($id);
244
        } else {
245
            $container = $this->getDefinitionDefaultContainer($id);
246
        }
247
248
        if ($container === null) {
249
            throw new NotFoundException($id);
250
        }
251
252
        return $container->get($id);
253
    }
254
255
    private function buildSubmoduleTree($submodule): array
256
    {
257
        $definitions = &$this->definitionsPlain[$submodule];
258
        if (isset($definitions[self::KEY_SUBMODULE_NAMES])) {
259
            foreach ($definitions[self::KEY_SUBMODULE_NAMES] as $subSubmodule) {
260
                $definitions[self::KEY_SUBMODULES][$subSubmodule] = $this->buildSubmoduleTree($subSubmodule);
261
            }
262
263
            unset($definitions[self::KEY_SUBMODULE_NAMES]);
264
        }
265
266
        return $definitions;
267
    }
268
269
    public function has($id)
270
    {
271
        // TODO: Implement has() method.
272
    }
273
}
274