Test Failed
Pull Request — master (#37)
by Divine Niiquaye
11:51
created

ExtensionBuilder::bootExtensions()   B

Complexity

Conditions 11
Paths 27

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 20
c 4
b 0
f 0
dl 0
loc 35
rs 7.3166
cc 11
nc 27
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2021 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade\DI\Extensions;
19
20
use Rade\DI\{AbstractContainer, ContainerBuilder};
21
use Rade\DI\Services\{AliasedInterface, DependenciesInterface};
22
use Symfony\Component\Config\Builder\{ConfigBuilderGenerator, ConfigBuilderGeneratorInterface};
23
use Symfony\Component\Config\Definition\{ConfigurationInterface, Processor};
24
use Symfony\Component\Config\Resource\{ClassExistenceResource, FileExistenceResource, FileResource, ResourceInterface};
25
26
/**
27
 * Provides ability to load container extensions.
28
 *
29
 * @author Divine Niiquaye Ibok <[email protected]>
30
 */
31
class ExtensionBuilder
32
{
33
    protected AbstractContainer $container;
34
35
    /** @var array<string,mixed> */
36
    private array $configuration;
37
38
    private ?ConfigBuilderGeneratorInterface $configBuilder = null;
39
40
    /** @var array<string,string> */
41
    private array $aliases = [];
42
43
    /** @var array<string,ExtensionInterface|null> */
44
    private array $extensions = [];
45
46
    /**
47
     * @param array<string,mixed> $config the default configuration for all extensions
48
     */
49
    public function __construct(AbstractContainer $container, array $config = [])
50
    {
51
        $this->container = $container;
52
        $this->configuration = $config;
53
    }
54
55
    /**
56
     * Enable Generating ConfigBuilders to help create valid config.
57
     */
58
    public function setConfigBuilderGenerator(string $outputDir): void
59
    {
60
        $this->configBuilder = new ConfigBuilderGenerator($outputDir);
61
    }
62
63
    /**
64
     * Get a registered extension instance for extending purpose or etc.
65
     */
66
    public function get(string $extensionName): ?ExtensionInterface
67
    {
68
        return $this->extensions[$this->aliases[$extensionName] ?? $extensionName] ?? null;
69
    }
70
71
    /**
72
     * Checks if extension exists.
73
     */
74
    public function has(string $extensionName): bool
75
    {
76
        return \array_key_exists($this->aliases[$extensionName] ?? $extensionName, $this->extensions);
77
    }
78
79
    /**
80
     * Get all loaded extensions.
81
     *
82
     * @return array<string,ExtensionInterface>
83
     */
84
    public function getExtensions(): array
85
    {
86
        return \array_filter($this->extensions);
87
    }
88
89
    /**
90
     * Get all extensions configuration.
91
     *
92
     * @return array<string,mixed>
93
     */
94
    public function getConfig(string $extensionName = null, string $parent = null, string &$key = null)
95
    {
96
        $configuration = $this->configuration;
97
98
        if (null === $extensionName) {
99
            return $configuration;
100
        }
101
102
        if (!(\array_key_exists($extensionName, $this->extensions) || \array_key_exists($extensionName, $this->aliases))) {
103
            throw new \InvalidArgumentException(\sprintf('The extension name provided in not valid, must be an extension\'s class name or alias.', $extensionName));
104
        }
105
106
        if (isset($parent, $configuration[$parent])) {
107
            $configuration = $configuration[$parent];
108
        }
109
110
        if (isset($configuration[$extensionName])) {
111
            $config = $configuration[$key = $extensionName];
112
        } elseif (isset($this->aliases[$extensionName], $configuration[$this->aliases[$extensionName]])) {
113
            $config = $configuration[$key = $this->aliases[$extensionName]];
114
        } elseif ($searchedId = \array_search($extensionName, $this->aliases, true)) {
115
            $config = $configuration[$key = $searchedId] ?? [];
116
        }
117
118
        return $config ?? [];
119
    }
120
121
    /**
122
     * Modify the default configuration for an extension.
123
     *
124
     * @param array<string,mixed> $configuration
125
     */
126
    public function modifyConfig(string $extensionName, array $configuration, string $parent = null): void
127
    {
128
        if (!\array_key_exists($extensionName, $this->extensions)) {
129
            throw new \InvalidArgumentException(\sprintf('The extension name provided in not valid, must be an extension\'s class name.', $extensionName));
130
        }
131
132
        $defaults = $this->getConfig($extensionName, $parent, $extensionKey);
133
134
        if (!empty($defaults)) {
135
            $values = \array_replace_recursive($defaults, $configuration);
136
137
            if (isset($parent, $extensionKey, $this->configuration[$parent][$extensionKey])) {
138
                $this->configuration[$parent][$extensionKey] = $values;
139
            } else {
140
                $this->configuration[$extensionKey] = $values;
141
            }
142
        } else {
143
            $this->configuration[$extensionName] = $configuration;
144
        }
145
    }
146
147
    /**
148
     * Loads a set of container extensions.
149
     *
150
     * You can map an extension class name to a priority index else if
151
     * declared as an array with arguments the third index is termed as priority value.
152
     *
153
     * Example:
154
     * [
155
     *    PhpExtension::class,
156
     *    CoreExtension::class => -1,
157
     *    [ProjectExtension::class, ['%project.dir%']],
158
     * ]
159
     *
160
     * @param array<int,mixed> $extensions
161
     */
162
    public function load(array $extensions): void
163
    {
164
        $this->container->runScope([ExtensionInterface::BUILDER => $this], function () use ($extensions): void {
165
            /** @var array<int,BootExtensionInterface> */
166
            $afterLoading = [];
167
168
            $this->bootExtensions($extensions, $afterLoading);
169
170
            foreach (\array_reverse($afterLoading) as $bootable) {
171
                $bootable->boot($this->container);
172
            }
173
        });
174
    }
175
176
    /**
177
     * Set alias if exist and return config if exists too.
178
     *
179
     * @return array<int|string,mixed>
180
     */
181
    protected function process(ExtensionInterface $extension, string $extraKey = null): array
182
    {
183
        $configuration = $this->configuration;
184
        $id = \get_class($extension);
185
186
        if ($extension instanceof AliasedInterface) {
187
            $aliasedId = $extension->getAlias();
188
189
            if (isset($this->aliases[$aliasedId])) {
190
                throw new \RuntimeException(\sprintf('The aliased id "%s" for %s extension class must be unqiue.', $aliasedId, \get_class($extension)));
191
            }
192
193
            $this->aliases[$aliasedId] = $id;
194
        }
195
196
        if (isset($extraKey, $configuration[$extraKey])) {
197
            $configuration = $configuration[$parent = $extraKey];
198
        }
199
200
        if (isset($aliasedId, $configuration[$aliasedId]) || \array_key_exists($id, $configuration)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $aliasedId does not seem to be defined for all execution paths leading up to this point.
Loading history...
201
            $configuration = $configuration[$aliasedId ?? ($e = $id)] ?? [];
202
        } else {
203
            $configuration = [];
204
        }
205
206
        if ($extension instanceof ConfigurationInterface) {
207
            if (null !== $this->configBuilder && \is_string($configuration)) {
208
                $configLoader = $this->configBuilder->build($extension)();
209
210
                if (\file_exists($configuration = $this->container->parameter($configuration))) {
211
                    (include $configuration)($configLoader);
212
                }
213
214
                $config = $configLoader->toArray();
215
            } else {
216
                $treeBuilder = $extension->getConfigTreeBuilder()->buildTree();
217
                $config = (new Processor())->process($treeBuilder, [$treeBuilder->getName() => $configuration]);
218
            }
219
220
            if (isset($parent)) {
221
                unset($this->configuration[$parent][$aliasedId ?? $e ?? $id]);
222
223
                return $this->configuration[$parent][$id] = $config;
224
            }
225
        }
226
227
        unset($this->configuration[$aliasedId ?? $e ?? $id]);
228
229
        return $this->configuration[$id] = $config ?? $configuration;
230
    }
231
232
    /**
233
     * Resolve extensions and register them.
234
     *
235
     * @param mixed[]                           $extensions
236
     * @param array<int,BootExtensionInterface> $afterLoading
237
     */
238
    private function bootExtensions(array $extensions, array &$afterLoading, string $extraKey = null): void
239
    {
240
        $container = $this->container;
241
242
        foreach ($this->sortExtensions($extensions) as $extension) {
243
            [$extension, $args] = \is_array($extension) ? $extension : [$extension, []];
244
245
            if (\is_subclass_of($extension, DebugExtensionInterface::class) && $extension::inDevelopment() !== $container->parameters['debug']) {
246
                continue;
247
            }
248
249
            if ($container instanceof ContainerBuilder) {
250
                if (\interface_exists(ResourceInterface::class)) {
251
                    $container->addResource(new ClassExistenceResource($extension, false));
252
                    $container->addResource(new FileExistenceResource($rPath = ($ref = new \ReflectionClass($extension))->getFileName()));
253
                    $container->addResource(new FileResource($rPath));
254
                }
255
256
                $ref = $ref ?? new \ReflectionClass($extension);
257
                $resolved = $ref->newInstanceArgs(\array_map(static fn ($value) => \is_string($value) ? $container->parameter($value) : $value, $args));
258
            } else {
259
                $resolved = $container->getResolver()->resolveClass($extension, $args);
260
            }
261
262
            /** @var ExtensionInterface $resolved */
263
            $this->extensions[$extension] = $resolved; // Add to stack before registering it ...
264
265
            if ($resolved instanceof DependenciesInterface) {
266
                $this->bootExtensions($resolved->dependencies(), $afterLoading, $extraKey = \method_exists($resolved, 'dependOnConfigKey') ? $resolved->dependOnConfigKey() : $extraKey);
267
            }
268
269
            $resolved->register($container, $this->process($resolved, $extraKey));
270
271
            if ($resolved instanceof BootExtensionInterface) {
272
                $afterLoading[] = $resolved;
273
            }
274
        }
275
    }
276
277
    /**
278
     * Sort extensions by priority.
279
     *
280
     * @param mixed[] $extensions container extensions with their priority as key
281
     *
282
     * @return array<int,mixed>
283
     */
284
    private function sortExtensions(array $extensions): array
285
    {
286
        if (0 === \count($extensions)) {
287
            return [];
288
        }
289
290
        $passes = [];
291
292
        foreach ($extensions as $offset => $extension) {
293
            $index = 0;
294
295
            if (\is_int($extension)) {
296
                [$index, $extension] = [$extension, $offset];
297
            } elseif (\is_array($extension) && isset($extension[2])) {
298
                $index = $extension[2];
299
            }
300
301
            $passes[$index][] = $extension;
302
            $this->extensions[\is_array($extension) ? $extension[0] : $extension] = null;
303
        }
304
305
        \krsort($passes);
306
307
        // Flatten the array
308
        return \array_merge(...$passes);
309
    }
310
}
311