• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valksor / php-bundle / 21323304205

24 Jan 2026 11:21PM UTC coverage: 61.471% (-3.6%) from 65.046%
21323304205

push

github

k0d3r1s
wip

3 of 3 new or added lines in 1 file covered. (100.0%)

113 existing lines in 4 files now uncovered.

493 of 802 relevant lines covered (61.47%)

1.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.72
/ValksorBundle.php
1
<?php declare(strict_types = 1);
2

3
/*
4
 * This file is part of the Valksor package.
5
 *
6
 * (c) Davis Zalitis (k0d3r1s)
7
 * (c) SIA Valksor <packages@valksor.com>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace Valksor\Bundle;
14

15
use FilesystemIterator;
16
use Psr\Cache\InvalidArgumentException;
17
use RecursiveDirectoryIterator;
18
use RecursiveIteratorIterator;
19
use ReflectionException;
20
use RuntimeException;
21
use Seld\JsonLint\ParsingException;
22
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
23
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
24
use Symfony\Component\DependencyInjection\ContainerBuilder;
25
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
26
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
27
use Symfony\Contracts\Cache\CacheInterface;
28
use Throwable;
29
use Valksor\Bundle\DependencyInjection\Dependency;
30
use Valksor\Bundle\DependencyInjection\ValksorConfiguration;
31
use Valksor\FullStack;
32
use Valksor\Functions\Iteration;
33
use Valksor\Functions\Local;
34
use Valksor\Functions\Memoize\MemoizeCache;
35

36
use function array_key_exists;
37
use function array_merge_recursive;
38
use function class_exists;
39
use function count;
40
use function dirname;
41
use function file_get_contents;
42
use function in_array;
43
use function is_a;
44
use function is_bool;
45
use function is_dir;
46
use function is_file;
47
use function ksort;
48
use function preg_replace;
49
use function rtrim;
50
use function sprintf;
51
use function str_ends_with;
52
use function str_replace;
53
use function str_starts_with;
54
use function strlen;
55
use function strtolower;
56
use function substr;
57

58
use const DIRECTORY_SEPARATOR;
59

60
final class ValksorBundle extends AbstractBundle
61
{
62
    public const string VALKSOR = 'valksor';
63

64
    private const array SELFS = [
65
        'valksor',
66
        'valksor-dev',
67
        'valksor-plugin',
68
    ];
69

70
    private ?MemoizeCache $cache = null;
71

72
    /** @var array<string, array{class: string, available: bool}>|null */
73
    private ?array $discoveredComponents = null;
74

75
    private ?string $projectDir = null;
76

77
    public function boot(): void
78
    {
UNCOV
79
        parent::boot();
×
UNCOV
80
        $this->memoize();
×
81
    }
82

83
    /**
84
     * @throws ParsingException
85
     */
86
    public function build(
87
        ContainerBuilder $container,
88
    ): void {
89
        static $_helper = null;
1✔
90

91
        if (null === $_helper) {
1✔
92
            $_helper = new class {
1✔
93
                use Local\Traits\_Exists;
94
                use Local\Traits\_WillBeAvailable;
95
            };
1✔
96
        }
97

98
        if (null === $this->projectDir) {
1✔
99
            $bag = $container->getParameterBag();
1✔
100

101
            if ($bag->has('kernel.project_dir')) {
1✔
UNCOV
102
                $this->projectDir = $bag->get('kernel.project_dir');
×
103
            }
104
        }
105

106
        foreach ($this->discoverComponents() as $component => $componentData) {
1✔
107
            $this->callback($component, $componentData, static function (object $object) use ($container): void {
1✔
108
                $object->build($container);
1✔
109
            });
1✔
110
        }
111

112
        new ValksorConfiguration()->build($container);
1✔
113
    }
114

115
    /**
116
     * @throws ParsingException
117
     */
118
    public function configure(
119
        DefinitionConfigurator $definition,
120
    ): void {
121
        /** @var ArrayNodeDefinition $rootNode */
122
        $rootNode = $definition
1✔
123
            ->rootNode();
1✔
124

125
        static $_helper = null;
1✔
126

127
        if (null === $_helper) {
1✔
128
            $_helper = new class {
1✔
129
                use Local\Traits\_Exists;
130
                use Local\Traits\_WillBeAvailable;
131
            };
1✔
132
        }
133

134
        $willBeAvailable = static function (string $package, string $class, ?string $parentPackage = null) use ($_helper) {
1✔
135
            $parentPackages = (array) $parentPackage;
×
UNCOV
136
            $parentPackages[] = sprintf('%s/bundle', self::VALKSOR);
×
137

UNCOV
138
            return $_helper->willBeAvailable($package, $class, $parentPackages);
×
139
        };
1✔
140

141
        $enableIfStandalone = static fn (string $package, string $class) => !class_exists(FullStack::class) && $willBeAvailable($package, $class) ? 'canBeDisabled' : 'canBeEnabled';
1✔
142

143
        $wrapper = static fn (string $package, string $componentClass) => $enableIfStandalone($package, '');
1✔
144
        new ValksorConfiguration()->addSection($rootNode, $wrapper, '');
1✔
145

146
        // Track which parent nodes need addDefaultsIfNotSet()
147
        $parentNodeNeedsDefaults = [];
1✔
148
        $allComponents = $this->discoverComponents();
1✔
149

150
        // First pass: collect requirements from all components
151
        foreach ($allComponents as $component => $componentData) {
1✔
152
            $this->callback($component, $componentData, function (object $object, string $class, string $component) use (&$parentNodeNeedsDefaults): void {
1✔
153
                $configPath = $this->getComponentConfigPath($class, $component);
1✔
154

155
                // Track all parent nodes this component touches
156
                for ($i = 0; $i < count($configPath) - 1; $i++) {
1✔
UNCOV
157
                    $pathPart = $configPath[$i];
×
158

UNCOV
159
                    if (!isset($parentNodeNeedsDefaults[$pathPart])) {
×
UNCOV
160
                        $parentNodeNeedsDefaults[$pathPart] = !$object->usesArrayPrototype();
×
161
                    } else {
162
                        // If ANY component needs defaults, mark it as needed
UNCOV
163
                        $parentNodeNeedsDefaults[$pathPart] = $parentNodeNeedsDefaults[$pathPart] || !$object->usesArrayPrototype();
×
164
                    }
165
                }
166
            });
1✔
167
        }
168

169
        $createdNodes = [];
1✔
170

171
        // Second pass: build the configuration tree with collected requirements
172
        foreach ($allComponents as $component => $componentData) {
1✔
173
            $this->callback($component, $componentData, function (object $object, string $class, string $component) use ($enableIfStandalone, $rootNode, &$createdNodes, $parentNodeNeedsDefaults): void {
1✔
174
                // Get namespace-based path
175
                $configPath = $this->getComponentConfigPath($class, $component);
1✔
176

177
                // Build nested structure
178
                $currentNode = $rootNode;
1✔
179

180
                // Navigate/create all parent nodes
181
                for ($i = 0; $i < count($configPath) - 1; $i++) {
1✔
UNCOV
182
                    $pathPart = $configPath[$i];
×
183

UNCOV
184
                    if (!isset($createdNodes[$pathPart])) {
×
UNCOV
185
                        $node = $currentNode->children()
×
UNCOV
186
                            ->arrayNode($pathPart);
×
187

188
                        // Apply addDefaultsIfNotSet() if any component needs it
UNCOV
189
                        if ($parentNodeNeedsDefaults[$pathPart] ?? false) {
×
UNCOV
190
                            $node->addDefaultsIfNotSet();
×
191
                        }
192

UNCOV
193
                        $currentNode = $node;
×
UNCOV
194
                        $createdNodes[$pathPart] = $currentNode;
×
195
                    } else {
UNCOV
196
                        $currentNode = $createdNodes[$pathPart];
×
197
                    }
198
                }
199

200
                // Add component at the final location
201
                $wrapper = static fn (string $package, string $componentClass) => $enableIfStandalone($package, $class);
1✔
202
                $object->addSection($currentNode, $wrapper, end($configPath));
1✔
203
            });
1✔
204
        }
205
    }
206

207
    /**
208
     * Get all configuration defaults from discovered components.
209
     *
210
     * Uses auto-discovery to collect defaults from all configuration classes
211
     * into a hierarchical array matching the configuration tree structure.
212
     *
213
     * @param CacheInterface|null $cache Optional cache pool for performance.
214
     *                                   Should be configured by the application using this bundle.
215
     *
216
     * @return array<string, mixed> Hierarchical array of all configuration defaults
217
     *
218
     * @throws InvalidArgumentException
219
     * @throws ParsingException
220
     */
221
    public function getAllConfigurationDefaults(
222
        ?CacheInterface $cache = null,
223
    ): array {
UNCOV
224
        if (null === $cache) {
×
UNCOV
225
            return $this->computeAllDefaults();
×
226
        }
227

UNCOV
228
        return $cache->get('valksor.bundle.defaults', fn () => $this->computeAllDefaults());
×
229
    }
230

231
    /**
232
     * @param array<string, mixed> $config
233
     *
234
     * @throws ParsingException
235
     */
236
    public function loadExtension(
237
        array $config,
238
        ContainerConfigurator $container,
239
        ContainerBuilder $builder,
240
    ): void {
241
        static $_helper = null;
2✔
242

243
        if (null === $_helper) {
2✔
244
            $_helper = new class {
1✔
245
                use Iteration\Traits\_MakeOneDimension;
246
                use Local\Traits\_Exists;
247
                use Local\Traits\_WillBeAvailable;
248
            };
1✔
249
        }
250

251
        foreach ($_helper->makeOneDimension([self::VALKSOR => $config]) as $key => $value) {
2✔
252
            $builder->setParameter($key, $value);
2✔
253
        }
254

255
        foreach ($this->discoverComponents() as $component => $componentData) {
2✔
256
            $this->callback($component, $componentData, static function (object $object, string $class, string $component) use ($container, $builder): void {
2✔
257
                $object->registerConfiguration($container, $builder, $component);
1✔
258
            }, $builder);
2✔
259
        }
260

261
        new ValksorConfiguration()->registerConfiguration($container, $builder, '');
2✔
262
    }
263

264
    /**
265
     * @throws ParsingException
266
     */
267
    public function prependExtension(
268
        ContainerConfigurator $container,
269
        ContainerBuilder $builder,
270
    ): void {
271
        $valksor = new ValksorConfiguration();
2✔
272

273
        $usesDoctrine = false;
2✔
274

275
        static $_helper = null;
2✔
276

277
        if (null === $_helper) {
2✔
278
            $_helper = new class {
1✔
279
                use Local\Traits\_Exists;
280
                use Local\Traits\_WillBeAvailable;
281
            };
1✔
282
        }
283

284
        if (null === $this->projectDir) {
2✔
285
            $bag = $builder->getParameterBag();
2✔
286

287
            if ($bag->has('kernel.project_dir')) {
2✔
UNCOV
288
                $this->projectDir = $bag->get('kernel.project_dir');
×
289
            }
290
        }
291

292
        foreach ($this->discoverComponents() as $component => $componentData) {
2✔
293
            $this->callback($component, $componentData, static function (object $object, string $class, string $component) use ($container, $builder, &$usesDoctrine): void {
2✔
294
                $object->registerPreConfiguration($container, $builder, $component);
2✔
295
                $usesDoctrine = $usesDoctrine || $object->usesDoctrine();
2✔
296
            }, $builder);
2✔
297
        }
298

299
        $valksor->registerPreConfiguration($container, $builder, '');
2✔
300

301
        if ($usesDoctrine) {
2✔
302
            $valksor->registerGlobalMigrations($container, $builder);
1✔
303
        }
304
    }
305

306
    /**
307
     * @return array<string, mixed>
308
     */
309
    public static function getConfig(
310
        string $package,
311
        ContainerBuilder $builder,
312
    ): array {
313
        return array_merge_recursive(...$builder->getExtensionConfig($package));
1✔
314
    }
315

316
    public static function p(
317
        ContainerBuilder $builder,
318
        string $component,
319
        string $parameter,
320
    ): mixed {
321
        return $builder->getParameter(sprintf('%s.%s.%s', self::VALKSOR, $component, $parameter));
8✔
322
    }
323

324
    /**
325
     * @param array<string, mixed> $componentData
326
     */
327
    private function callback(
328
        string $component,
329
        array $componentData,
330
        callable $callback,
331
        ?ContainerBuilder $builder = null,
332
    ): void {
333
        static $_helper = null;
11✔
334

335
        if (null === $_helper) {
11✔
336
            $_helper = new class {
1✔
337
                use Local\Traits\_Exists;
338
                use Local\Traits\_WillBeAvailable;
339
            };
1✔
340
        }
341

342
        $class = $componentData['class'];
11✔
343

344
        if (!$_helper->exists($class)) {
11✔
345
            return;
1✔
346
        }
347

348
        if (!$componentData['available']) {
10✔
349
            return;
1✔
350
        }
351

352
        $package = self::VALKSOR . '/' . $component;
9✔
353

354
        if (!$_helper->willBeAvailable($package, $class, [sprintf('%s/bundle', self::VALKSOR)])) {
9✔
UNCOV
355
            return;
×
356
        }
357

358
        $object = new $class();
9✔
359

360
        if (is_a($object, Dependency::class)) {
9✔
361
            if (null !== $builder) {
9✔
362
                try {
363
                    $enabled = self::p($builder, $component, 'enabled');
7✔
364

365
                    if (!is_bool($enabled) || !$enabled) {
5✔
366
                        return;
5✔
367
                    }
368
                } catch (Throwable) {
2✔
369
                }
370
            }
371

372
            $callback($object, $class, $component);
6✔
373
        }
374
    }
375

376
    /**
377
     * Compute all configuration defaults from discovered components.
378
     *
379
     * @return array<string, mixed> Hierarchical array of all configuration defaults
380
     *
381
     * @throws ParsingException
382
     */
383
    private function computeAllDefaults(): array
384
    {
UNCOV
385
        $defaults = [];
×
386

387
        foreach ($this->discoverComponents() as $componentId => $componentData) {
×
388
            $className = $componentData['class'];
×
389

UNCOV
390
            $componentDefaults = $className::getDefaults();
×
391

UNCOV
392
            if ([] === $componentDefaults) {
×
393
                continue;
×
394
            }
395

UNCOV
396
            $current = &$defaults;
×
397

398
            foreach ($this->getComponentConfigPath($className, $componentId) as $key) {
×
399
                if (!isset($current[$key])) {
×
UNCOV
400
                    $current[$key] = [];
×
401
                }
UNCOV
402
                $current = &$current[$key];
×
403
            }
UNCOV
404
            $current = $componentDefaults;
×
405
        }
406

UNCOV
407
        return $defaults;
×
408
    }
409

410
    /**
411
     * @return array<string, array{class: string, available: bool}> Array of component ID => {class, available}
412
     *
413
     * @throws ParsingException
414
     */
415
    private function discoverComponents(): array
416
    {
417
        if (null !== $this->discoveredComponents) {
7✔
418
            return $this->discoveredComponents;
7✔
419
        }
420

421
        $this->discoveredComponents = [];
×
UNCOV
422
        $visitedClasses = [];
×
423

UNCOV
424
        $autoloadPsr4 = require $this->findProjectRoot() . '/vendor/composer/autoload_psr4.php';
×
425

426
        static $_helper = null;
×
427

428
        if (null === $_helper) {
×
429
            $_helper = new class {
×
430
                use Traits\_LoadReflection;
431
            };
×
432
        }
433

UNCOV
434
        foreach ($autoloadPsr4 as $namespacePrefix => $directories) {
×
435
            if (!str_starts_with($namespacePrefix, 'Valksor\\') && !str_starts_with($namespacePrefix, 'ValksorDev\\')) {
×
UNCOV
436
                continue;
×
437
            }
438

UNCOV
439
            foreach ($directories as $directory) {
×
UNCOV
440
                foreach ($this->findConfigurationClasses($directory, $namespacePrefix) as $className) {
×
UNCOV
441
                    if (array_key_exists($className, $visitedClasses)) {
×
UNCOV
442
                        continue;
×
443
                    }
444

UNCOV
445
                    $visitedClasses[$className] = true;
×
446

447
                    try {
UNCOV
448
                        $reflection = $_helper->loadReflection($className, $this->memoize());
×
UNCOV
449
                    } catch (ReflectionException) {
×
UNCOV
450
                        continue;
×
451
                    }
452

UNCOV
453
                    if (!$reflection->implementsInterface(Dependency::class) || $reflection->isAbstract()) {
×
UNCOV
454
                        continue;
×
455
                    }
456

UNCOV
457
                    $componentName = substr($reflection->getShortName(), 0, -13);
×
UNCOV
458
                    $componentId = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $componentName));
×
459

UNCOV
460
                    if (self::VALKSOR === $componentId || isset($this->discoveredComponents[$componentId])) {
×
UNCOV
461
                        continue;
×
462
                    }
463

464
                    try {
UNCOV
465
                        $available = new $className()->autoDiscover();
×
UNCOV
466
                    } catch (Throwable) {
×
UNCOV
467
                        $available = false;
×
468
                    }
469

UNCOV
470
                    $this->discoveredComponents[$componentId] = [
×
UNCOV
471
                        'class' => $className,
×
UNCOV
472
                        'available' => $available,
×
UNCOV
473
                    ];
×
474
                }
475
            }
476
        }
477

UNCOV
478
        ksort($this->discoveredComponents);
×
479

UNCOV
480
        return $this->discoveredComponents;
×
481
    }
482

483
    /**
484
     * @return iterable<string>
485
     */
486
    private function findConfigurationClasses(
487
        string $directory,
488
        string $namespacePrefix,
489
    ): iterable {
UNCOV
490
        $normalizedDirectory = rtrim($directory, DIRECTORY_SEPARATOR . '/');
×
491

UNCOV
492
        if ('' === $normalizedDirectory || !is_dir($normalizedDirectory)) {
×
UNCOV
493
            return;
×
494
        }
495

UNCOV
496
        $iterator = new RecursiveIteratorIterator(
×
UNCOV
497
            new RecursiveDirectoryIterator($normalizedDirectory, FilesystemIterator::SKIP_DOTS),
×
UNCOV
498
        );
×
499

UNCOV
500
        foreach ($iterator as $file) {
×
UNCOV
501
            if (!$file->isFile() || 'php' !== $file->getExtension()) {
×
UNCOV
502
                continue;
×
503
            }
504

UNCOV
505
            $basename = $file->getBasename('.php');
×
506

UNCOV
507
            if (!str_ends_with($basename, 'Configuration')) {
×
UNCOV
508
                continue;
×
509
            }
510

UNCOV
511
            $relativePath = substr($file->getPathname(), strlen($normalizedDirectory) + 1);
×
UNCOV
512
            $relativeClass = substr($relativePath, 0, -4);
×
UNCOV
513
            $relativeClass = str_replace(DIRECTORY_SEPARATOR, '\\', $relativeClass);
×
514

UNCOV
515
            yield rtrim($namespacePrefix, '\\') . '\\' . $relativeClass;
×
516
        }
517
    }
518

519
    /**
520
     * Recursively find the project root by looking for composer.json.
521
     *
522
     * @throws ParsingException
523
     */
524
    private function findProjectRoot(): string
525
    {
UNCOV
526
        if (null !== $this->projectDir) {
×
UNCOV
527
            return $this->projectDir;
×
528
        }
529

UNCOV
530
        $dir = __DIR__;
×
531

UNCOV
532
        static $_helper = null;
×
533

UNCOV
534
        if (null === $_helper) {
×
UNCOV
535
            $_helper = new class {
×
536
                use Iteration\Traits\_JsonDecode;
UNCOV
537
            };
×
538
        }
539

UNCOV
540
        while ($dir !== dirname($dir)) {
×
541
            // Check if this is the actual project root (has vendor directory)
UNCOV
542
            if (is_file($dir . '/composer.json')) {
×
UNCOV
543
                $data = $_helper->jsonDecode(file_get_contents($dir . '/composer.json'), true);
×
544

UNCOV
545
                if (is_dir($dir . '/vendor') && !in_array($data['name'], self::SELFS, true)) {
×
UNCOV
546
                    return $this->projectDir = $dir;
×
547
                }
548
            }
UNCOV
549
            $dir = dirname($dir);
×
550
        }
551

UNCOV
552
        throw new RuntimeException('Could not find project root (composer.json with vendor directory)');
×
553
    }
554

555
    /**
556
     * Extract configuration path from component namespace.
557
     *
558
     * Maps namespace structure to configuration path, e.g.:
559
     * - Valksor\Component\FormType\CloudflareTurnstile\DependencyInjection\CloudflareTurnstileConfiguration
560
     *   → ['form_type', 'cloudflare_turnstile']
561
     * - Valksor\Component\Sse\DependencyInjection\SseConfiguration
562
     *   → ['sse']
563
     */
564
    private function getComponentConfigPath(
565
        string $className,
566
        string $componentName,
567
    ): array {
568
        // Handle Valksor components: Valksor\Component\<Category>\...\<Component>Configuration
569
        $valksorPattern = '#^Valksor\\\\Component\\\\([^\\\\]+)\\\\[^\\\\]+\\\\DependencyInjection\\\\[^\\\\]+$#';
1✔
570

571
        if (preg_match($valksorPattern, $className, $matches)) {
1✔
UNCOV
572
            $category = $matches[1]; // e.g., "FormType"
×
573

574
            // Convert PascalCase to snake_case
UNCOV
575
            $configSection = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $category));
×
576

UNCOV
577
            return [$configSection, $componentName];
×
578
        }
579

580
        // Default: flat structure
581
        return [$componentName];
1✔
582

583
        // Default: flat structure
584
    }
585

586
    private function memoize(): MemoizeCache
587
    {
588
        return $this->cache ??= new MemoizeCache();
1✔
589
    }
590
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc