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

ExtensionBuilder::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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\Exceptions\MissingPackageException;
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};
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
        if (\array_key_exists('parameters', $config)) {
52
            $container->parameters += $config['parameters'];
53
            unset($config['parameters']);
54
        }
55
56
        $this->container = $container;
57
        $this->configuration = $config;
58
    }
59
60
    /**
61
     * Enable Generating ConfigBuilders to help create valid config.
62
     */
63
    public function setConfigBuilderGenerator(string $outputDir): void
64
    {
65
        $this->configBuilder = new ConfigBuilderGenerator($outputDir);
66
    }
67
68
    /**
69
     * Get a registered extension instance for extending purpose or etc.
70
     */
71
    public function get(string $extensionName): ?ExtensionInterface
72
    {
73
        return $this->extensions[$this->aliases[$extensionName] ?? $extensionName] ?? null;
74
    }
75
76
    /**
77
     * Checks if extension exists.
78
     */
79
    public function has(string $extensionName): bool
80
    {
81
        return \array_key_exists($this->aliases[$extensionName] ?? $extensionName, $this->extensions);
82
    }
83
84
    /**
85
     * Get all loaded extensions.
86
     *
87
     * @return array<string,ExtensionInterface>
88
     */
89
    public function getExtensions(): array
90
    {
91
        return $this->extensions;
92
    }
93
94
    /**
95
     * Get all extensions configuration.
96
     *
97
     * @return array<string,mixed>
98
     */
99
    public function getConfig(string $extensionName = null, string $parent = null, string &$key = null)
100
    {
101
        $configuration = $this->configuration;
102
103
        if (null === $extensionName) {
104
            return $configuration;
105
        }
106
107
        if (!(\array_key_exists($extensionName, $this->extensions) || \array_key_exists($extensionName, $this->aliases))) {
108
            throw new \InvalidArgumentException(\sprintf('The extension name provided in not valid, must be an extension\'s class name or alias.', $extensionName));
109
        }
110
111
        if (isset($parent, $configuration[$parent])) {
112
            $configuration = $configuration[$parent];
113
        }
114
115
        if (isset($configuration[$extensionName])) {
116
            $config = $configuration[$key = $extensionName];
117
        } elseif (isset($this->aliases[$extensionName], $configuration[$this->aliases[$extensionName]])) {
118
            $config = $configuration[$key = $this->aliases[$extensionName]];
119
        } elseif ($searchedId = \array_search($extensionName, $this->aliases, true)) {
120
            $config = $configuration[$key = $searchedId] ?? [];
121
        }
122
123
        return $config ?? [];
124
    }
125
126
    /**
127
     * Modify the default configuration for an extension.
128
     *
129
     * @param array<string,mixed> $configuration
130
     * @param bool $replace If true, integer keys values will be replaceable
131
     */
132
    public function modifyConfig(string $extensionName, array $configuration, string $parent = null, bool $replace = false): void
133
    {
134
        if (!\array_key_exists($extensionName, $this->extensions)) {
135
            throw new \InvalidArgumentException(\sprintf('The extension name provided in not valid, must be an extension\'s class name.', $extensionName));
136
        }
137
138
        $defaults = $this->getConfig($extensionName, $parent, $extensionKey);
139
140
        if (!empty($defaults)) {
141
            $values = $this->mergeConfig($defaults, $configuration, $replace);
142
143
            if (isset($parent, $extensionKey, $this->configuration[$parent][$extensionKey])) {
144
                $this->configuration[$parent][$extensionKey] = $values;
145
            } else {
146
                $this->configuration[$extensionKey] = $values;
147
            }
148
        } else {
149
            $this->configuration[$extensionName] = $configuration;
150
        }
151
    }
152
153
    /**
154
     * Loads a set of container extensions.
155
     *
156
     * You can map an extension class name to a priority index else if
157
     * declared as an array with arguments the third index is termed as priority value.
158
     *
159
     * Example:
160
     * [
161
     *    PhpExtension::class,
162
     *    CoreExtension::class => -1,
163
     *    [ProjectExtension::class, ['%project.dir%']],
164
     * ]
165
     *
166
     * @param array<int,mixed> $extensions
167
     */
168
    public function load(array $extensions): void
169
    {
170
        $this->container->runScope([ExtensionInterface::BUILDER => $this], function () use ($extensions): void {
171
            /** @var array<int,BootExtensionInterface> */
172
            $afterLoading = [];
173
174
            $this->bootExtensions($extensions, $afterLoading);
175
176
            foreach (\array_reverse($afterLoading) as $bootable) {
177
                $bootable->boot($this->container);
178
            }
179
        });
180
    }
181
182
    /**
183
     * Set alias if exist and return config if exists too.
184
     *
185
     * @return array<int|string,mixed>
186
     */
187
    protected function process(ExtensionInterface $extension, string $extraKey = null): array
188
    {
189
        $configuration = $this->configuration;
190
        $id = \get_class($extension);
191
192
        if ($extension instanceof AliasedInterface) {
193
            $aliasedId = $extension->getAlias();
194
195
            if (isset($this->aliases[$aliasedId])) {
196
                throw new \RuntimeException(\sprintf('The aliased id "%s" for %s extension class must be unqiue.', $aliasedId, \get_class($extension)));
197
            }
198
199
            $this->aliases[$aliasedId] = $id;
200
        }
201
202
        if (isset($extraKey, $configuration[$extraKey])) {
203
            $configuration = $configuration[$parent = $extraKey];
204
        }
205
206
        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...
207
            $configuration = $configuration[$aliasedId ?? ($e = $id)] ?? [];
208
        } else {
209
            $configuration = [];
210
        }
211
212
        if ($extension instanceof ConfigurationInterface) {
213
            if (null !== $this->configBuilder && \is_string($configuration)) {
214
                $configLoader = $this->configBuilder->build($extension)();
215
216
                if (\file_exists($configuration = $this->container->parameter($configuration))) {
217
                    (include $configuration)($configLoader);
218
                }
219
220
                $configuration = $configLoader->toArray();
221
            } else {
222
                $treeBuilder = $extension->getConfigTreeBuilder()->buildTree();
223
                $configuration = (new Processor())->process($treeBuilder, [$treeBuilder->getName() => $configuration]);
224
            }
225
        }
226
227
        if (isset($parent)) {
228
            unset($this->configuration[$parent][$aliasedId ?? $e ?? $id]);
229
230
            return $this->configuration[$parent][$id] = $configuration;
231
        }
232
        unset($this->configuration[$aliasedId ?? $e ?? $id]);
233
234
        return $this->configuration[$id] = $configuration;
235
    }
236
237
    /**
238
     * Resolve extensions and register them.
239
     *
240
     * @param mixed[]                           $extensions
241
     * @param array<int,BootExtensionInterface> $afterLoading
242
     */
243
    private function bootExtensions(array $extensions, array &$afterLoading, string $extraKey = null): void
244
    {
245
        $container = $this->container;
246
247
        foreach ($this->sortExtensions($extensions) as $resolved) {
248
            if ($resolved instanceof DebugExtensionInterface && $resolved->inDevelopment() !== $container->parameters['debug']) {
249
                continue;
250
            }
251
252
            if ($container instanceof ContainerBuilder) {
253
                $container->addResource(new ClassExistenceResource(($ref = new \ReflectionClass($resolved))->getName(), false));
254
                $container->addResource(new FileExistenceResource($rPath = $ref->getFileName()));
255
                $container->addResource(new FileResource($rPath));
256
            }
257
258
            if ($resolved instanceof RequiredPackagesInterface) {
259
                $this->ensureRequiredPackagesAvailable($resolved);
260
            }
261
262
            $this->extensions[\get_class($resolved)] = $resolved; // Add to stack before registering it ...
263
264
            if ($resolved instanceof DependenciesInterface) {
265
                $this->bootExtensions($resolved->dependencies(), $afterLoading, \method_exists($resolved, 'dependOnConfigKey') ? $resolved->dependOnConfigKey() : $extraKey);
266
            }
267
268
            $resolved->register($container, $this->process($resolved, $extraKey));
269
270
            if ($resolved instanceof BootExtensionInterface) {
271
                $afterLoading[] = $resolved;
272
            }
273
        }
274
    }
275
276
    /**
277
     * Sort extensions by priority.
278
     *
279
     * @param mixed[] $extensions container extensions with their priority as key
280
     *
281
     * @return array<string,ExtensionInterface>
282
     */
283
    private function sortExtensions(array $extensions): array
284
    {
285
        if (0 === \count($extensions)) {
286
            return [];
287
        }
288
289
        $passes = [];
290
291
        foreach ($extensions as $offset => $extension) {
292
            $index = 0;
293
294
            if (\is_int($extension)) {
295
                [$index, $extension] = [$extension, $offset];
296
            } elseif (\is_array($extension) && isset($extension[2])) {
297
                $index = $extension[2];
298
            }
299
            [$extension, $args] = \is_array($extension) ? $extension : [$extension, []];
300
301
            if ($this->container instanceof ContainerBuilder) {
302
                $resolved = (new \ReflectionClass($extension))->newInstanceArgs(\array_map(fn ($v) => \is_string($v) ? $this->container->parameter($v) : $v, $args));
303
            } else {
304
                $resolved = $this->container->getResolver()->resolveClass($extension, $args);
305
            }
306
307
            $passes[$index][] = $this->extensions[$extension] = $resolved;
308
        }
309
        \krsort($passes);
310
311
        return \array_merge(...$passes); // Flatten the array
312
    }
313
314
    private function ensureRequiredPackagesAvailable(RequiredPackagesInterface $extension): void
315
    {
316
        $missingPackages = [];
317
318
        foreach ($extension->getRequiredPackages() as $requiredClass => $packageName) {
319
            if (!\class_exists($requiredClass)) {
320
                $missingPackages[] = $packageName;
321
            }
322
        }
323
324
        if (!$missingPackages) {
325
            return;
326
        }
327
328
        throw new MissingPackageException(\sprintf('Missing package%s, to use the "%s" extension, run: composer require %s', \count($missingPackages) > 1 ? 's' : '', \get_class($extension), \implode(' ', $missingPackages)));
329
    }
330
331
    /**
332
     * Merges $b into $a.
333
     */
334
    private function mergeConfig(array $a, array $b, bool $replace): array
335
    {
336
        foreach ($b as $k => $v) {
337
            if (\array_key_exists($k, $a)) {
338
                if (!\is_array($v)) {
339
                    $replace || \is_string($k) ? $a[$k] = $v : $a[] = $v;
340
341
                    continue;
342
                }
343
                $a[$k] = $this->mergeConfig($a[$k], $v, $replace);
344
            } else {
345
                $a[$k] = $v;
346
            }
347
        }
348
349
        return $a;
350
    }
351
}
352