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

valksor / php-bundle / 19113717014

05 Nov 2025 01:29PM UTC coverage: 78.447% (-0.2%) from 78.618%
19113717014

push

github

k0d3r1s
improve app loader

21 of 28 new or added lines in 1 file covered. (75.0%)

495 of 631 relevant lines covered (78.45%)

1.94 hits per line

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

91.67
/Kernel/AbstractKernel.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\Kernel;
14

15
use LogicException;
16
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
17
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
18
use Symfony\Component\Dotenv\Dotenv;
19
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
20
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
21
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
22

23
use function array_merge;
24
use function explode;
25
use function file_exists;
26
use function glob;
27
use function is_dir;
28
use function is_file;
29
use function pathinfo;
30
use function sort;
31
use function sprintf;
32
use function strlen;
33
use function ucfirst;
34

35
use const GLOB_NOSORT;
36
use const PATHINFO_FILENAME;
37

38
abstract class AbstractKernel extends BaseKernel
39
{
40
    use MicroKernelTrait;
41
    protected ?string $apps = null;
42

43
    protected ?string $infrastructure = null;
44

45
    /** @var array<string, BundleInterface>|null */
46
    private ?array $allBundles = null;
47

48
    public function __construct(
49
        string $environment,
50
        bool $debug,
51
        private readonly string $id,
52
    ) {
53
        $_SERVER['APP_KERNEL_NAME'] = $this->id;
8✔
54
        $_ENV['APP_KERNEL_NAME'] = $this->id;
8✔
55

56
        if (null === $this->apps) {
8✔
57
            throw new LogicException('Apps dir not set');
×
58
        }
59

60
        if (null === $this->infrastructure) {
8✔
61
            throw new LogicException('Infrastructure dir not set');
×
62
        }
63

64
        parent::__construct($environment, $debug);
8✔
65

66
        // Load app-specific .env files
67
        $this->loadAppEnvironmentFiles();
8✔
68
    }
69

70
    public function getAppConfigDir(): string
71
    {
72
        return $this->getProjectDir() . '/' . $this->apps . '/' . $this->id . '/config';
6✔
73
    }
74

75
    public function getCacheDir(): string
76
    {
77
        return (($_SERVER['APP_CACHE_DIR'] ?? $this->getProjectDir()) . '/var/cache') . '/' . $this->id . '/' . $this->environment;
2✔
78
    }
79

80
    public function getConfigDir(): string
81
    {
82
        return $this->getProjectDir() . '/' . $this->infrastructure . '/config';
2✔
83
    }
84

85
    public function getLogDir(): string
86
    {
87
        return (($_SERVER['APP_LOG_DIR'] ?? $this->getProjectDir()) . '/var/log') . '/' . $this->id;
2✔
88
    }
89

90
    public function registerBundles(): iterable
91
    {
92
        foreach ($this->getAllBundles() as $class => $envs) {
1✔
93
            if ($envs[$this->environment] ?? $envs['all'] ?? false) {
1✔
94
                yield new $class();
1✔
95
            }
96
        }
97
    }
98

99
    protected function configureContainer(
100
        ContainerConfigurator $container,
101
    ): void {
102
        $infrastructureDir = $this->getProjectDir() . '/' . $this->infrastructure . '/config';
1✔
103
        $appDir = $this->getAppConfigDir();
1✔
104

105
        // Set app-specific parameters
106
        $container->parameters()
1✔
107
            ->set('app.id', $this->id)
1✔
108
            ->set('app.namespace', ucfirst(explode('.', $this->id, 2)[0]));
1✔
109

110
        $this->importInfrastructurePackagesWithOverride($infrastructureDir, $appDir, $container);
1✔
111
        $this->importConfig($infrastructureDir . '/services.%s', $container);
1✔
112
        $this->importConfigWithWildcardSupport($appDir . '/{packages}/*.%s', $container);
1✔
113
        $this->importConfigWithWildcardSupport($appDir . '/{packages}/' . $this->environment . '/*.%s', $container);
1✔
114
        $this->importConfig($appDir . '/services.%s', $container);
1✔
115
    }
116

117
    protected function configureRoutes(
118
        RoutingConfigurator $routes,
119
    ): void {
120
        $infrastructureDir = $this->getProjectDir() . '/' . $this->infrastructure . '/config';
1✔
121
        $appDir = $this->getAppConfigDir();
1✔
122

123
        $this->importInfrastructureRoutesWithOverride($infrastructureDir, $appDir, $routes);
1✔
124
        // Import app routes normally
125
        $this->importRoutesWithWildcardSupport($appDir . '/{routes}/*.%s', $routes);
1✔
126
        $this->importRoutesWithWildcardSupport($appDir . '/{routes}/' . $this->environment . '/*.%s', $routes);
1✔
127
        $this->importRoutes($appDir . '/routes.%s', $routes);
1✔
128
    }
129

130
    /**
131
     * @return array<string, mixed>
132
     */
133
    protected function getKernelParameters(): array
134
    {
135
        return array_merge(parent::getKernelParameters(), [
1✔
136
            '.kernel.bundles_definition' => $this->getAllBundles(),
1✔
137
            '.kernel.config_dir' => $this->getConfigDir(),
1✔
138
        ]);
1✔
139
    }
140

141
    private function extractBaseDirectoryFromWildcard(
142
        string $wildcardPath,
143
    ): ?string {
144
        // Extract the base directory before the first wildcard
145
        $wildcardStart = strpos($wildcardPath, '{');
2✔
146

147
        if (false === $wildcardStart) {
2✔
NEW
148
            $wildcardStart = strpos($wildcardPath, '*');
×
149
        }
150

151
        if (false === $wildcardStart) {
2✔
NEW
152
            return null;
×
153
        }
154

155
        // Find the directory separator before the wildcard
156
        $dirSeparator = strrpos($wildcardPath, '/', $wildcardStart - strlen($wildcardPath));
2✔
157

158
        if (false === $dirSeparator) {
2✔
NEW
159
            return null;
×
160
        }
161

162
        return substr($wildcardPath, 0, $dirSeparator);
2✔
163
    }
164

165
    /**
166
     * @return array<string, BundleInterface>
167
     */
168
    private function getAllBundles(): array
169
    {
170
        if (null !== $this->allBundles) {
3✔
171
            return $this->allBundles;
1✔
172
        }
173

174
        $this->allBundles = [];
3✔
175

176
        $infrastructureBundlesFile = $this->getProjectDir() . '/' . $this->infrastructure . '/config/bundles.php';
3✔
177

178
        if (is_file($infrastructureBundlesFile)) {
3✔
179
            $infrastructureBundles = require $infrastructureBundlesFile;
3✔
180
            $this->allBundles = array_merge($this->allBundles, $infrastructureBundles);
3✔
181
        }
182

183
        $appBundlesFile = $this->getAppConfigDir() . '/bundles.php';
3✔
184

185
        if (is_file($appBundlesFile)) {
3✔
186
            $appBundles = require $appBundlesFile;
1✔
187
            $this->allBundles = array_merge($this->allBundles, $appBundles);
1✔
188
        }
189

190
        return $this->allBundles;
3✔
191
    }
192

193
    private function hasOverride(
194
        string $dir,
195
        string $base,
196
    ): bool {
197
        return is_file($dir . '/' . $base . '.override.php');
3✔
198
    }
199

200
    private function importConfig(
201
        string $filename,
202
        ContainerConfigurator $container,
203
        bool $check = true,
204
    ): void {
205
        $file = sprintf($filename, 'php');
1✔
206

207
        if (!$check || file_exists($file)) {
1✔
208
            $container->import($file);
1✔
209
        }
210
    }
211

212
    private function importConfigWithWildcardSupport(
213
        string $filename,
214
        ContainerConfigurator $container,
215
    ): void {
216
        $file = sprintf($filename, 'php');
1✔
217

218
        // Check if this is a wildcard pattern that needs directory validation
219
        if ($this->isWildcardPattern($file)) {
1✔
220
            $baseDir = $this->extractBaseDirectoryFromWildcard($file);
1✔
221

222
            if ($baseDir && is_dir($baseDir)) {
1✔
223
                $container->import($file);
1✔
224
            }
225
        // If base directory doesn't exist, skip the import silently
NEW
226
        } elseif (file_exists($file)) {
×
NEW
227
            $container->import($file);
×
228
        }
229
    }
230

231
    private function importDirConfigsWithOverride(
232
        string $infrastructureDir,
233
        string $appDir,
234
        ContainerConfigurator $container,
235
    ): void {
236
        foreach ($this->listInfrastructureFilesWithOverride($infrastructureDir, $appDir) as $infrastructureFile) {
1✔
237
            $container->import($infrastructureFile);
1✔
238
        }
239
    }
240

241
    private function importDirRoutesWithOverride(
242
        string $infrastructureDir,
243
        string $appDir,
244
        RoutingConfigurator $routes,
245
    ): void {
246
        foreach ($this->listInfrastructureFilesWithOverride($infrastructureDir, $appDir) as $infrastructureFile) {
1✔
247
            $routes->import($infrastructureFile);
1✔
248
        }
249
    }
250

251
    private function importInfrastructurePackagesWithOverride(
252
        string $infrastructureConfigDir,
253
        string $appConfigDir,
254
        ContainerConfigurator $container,
255
    ): void {
256
        $this->importDirConfigsWithOverride(
1✔
257
            $infrastructureConfigDir . '/packages',
1✔
258
            $appConfigDir . '/packages',
1✔
259
            $container,
1✔
260
        );
1✔
261

262
        $this->importDirConfigsWithOverride(
1✔
263
            $infrastructureConfigDir . '/packages/' . $this->environment,
1✔
264
            $appConfigDir . '/packages/' . $this->environment,
1✔
265
            $container,
1✔
266
        );
1✔
267
    }
268

269
    private function importInfrastructureRoutesWithOverride(
270
        string $infrastructureConfigDir,
271
        string $appConfigDir,
272
        RoutingConfigurator $routes,
273
    ): void {
274
        $this->importDirRoutesWithOverride(
1✔
275
            $infrastructureConfigDir . '/routes',
1✔
276
            $appConfigDir . '/routes',
1✔
277
            $routes,
1✔
278
        );
1✔
279

280
        $this->importDirRoutesWithOverride(
1✔
281
            $infrastructureConfigDir . '/routes/' . $this->environment,
1✔
282
            $appConfigDir . '/routes/' . $this->environment,
1✔
283
            $routes,
1✔
284
        );
1✔
285

286
        $infrastructureFile = $infrastructureConfigDir . '/routes.php';
1✔
287

288
        if (is_file($infrastructureFile) && !$this->hasOverride($appConfigDir, 'routes')) {
1✔
289
            $routes->import($infrastructureFile);
×
290
        }
291
    }
292

293
    private function importRoutes(
294
        string $filename,
295
        RoutingConfigurator $routes,
296
        bool $check = true,
297
    ): void {
298
        $file = sprintf($filename, 'php');
1✔
299

300
        if (!$check || file_exists($file)) {
1✔
301
            $routes->import($file);
1✔
302
        }
303
    }
304

305
    private function importRoutesWithWildcardSupport(
306
        string $filename,
307
        RoutingConfigurator $routes,
308
    ): void {
309
        $file = sprintf($filename, 'php');
1✔
310

311
        // Check if this is a wildcard pattern that needs directory validation
312
        if ($this->isWildcardPattern($file)) {
1✔
313
            $baseDir = $this->extractBaseDirectoryFromWildcard($file);
1✔
314

315
            if ($baseDir && is_dir($baseDir)) {
1✔
316
                $routes->import($file);
1✔
317
            }
318
        // If base directory doesn't exist, skip the import silently
NEW
319
        } elseif (file_exists($file)) {
×
NEW
320
            $routes->import($file);
×
321
        }
322
    }
323

324
    private function isWildcardPattern(
325
        string $filename,
326
    ): bool {
327
        return str_contains($filename, '{') || str_contains($filename, '*');
2✔
328
    }
329

330
    /**
331
     * Build a sorted list of infrastructure config files (php) from a directory,
332
     * excluding any whose base names are overridden by app-level override files.
333
     * Results are sorted alphabetically.
334
     *
335
     * @return array<string>
336
     */
337
    private function listInfrastructureFilesWithOverride(
338
        string $infrastructureDir,
339
        string $appDir,
340
    ): array {
341
        if (!is_dir($infrastructureDir)) {
3✔
342
            return [];
×
343
        }
344

345
        $files = glob($infrastructureDir . '/*.php', GLOB_NOSORT) ?: [];
3✔
346
        sort($files);
3✔
347

348
        $result = [];
3✔
349

350
        foreach ($files as $infrastructureFile) {
3✔
351
            $base = pathinfo($infrastructureFile, PATHINFO_FILENAME);
3✔
352

353
            if ($this->hasOverride($appDir, $base)) {
3✔
354
                continue;
3✔
355
            }
356
            $result[] = $infrastructureFile;
3✔
357
        }
358

359
        return $result;
3✔
360
    }
361

362
    /**
363
     * Load app-specific environment files following Symfony's standard hierarchy.
364
     *
365
     * Files are loaded in order (later files override earlier ones):
366
     * 1. /infrastructure/.env                            - Infrastructure environment file
367
     * 2. /infrastructure/.env.local                      - Local infrastructure overrides (gitignored)
368
     * 3. /infrastructure/.env.{environment}              - Environment-specific infrastructure (e.g., .env.dev, .env.prod)
369
     * 4. /infrastructure/.env.{environment}.local        - Environment-specific local infrastructure overrides (gitignored)
370
     * 5. /apps/{app_id}/.env                     - Base environment file
371
     * 6. /apps/{app_id}/.env.local               - Local overrides (gitignored)
372
     * 7. /apps/{app_id}/.env.{environment}       - Environment-specific (e.g., .env.dev, .env.prod)
373
     * 8. /apps/{app_id}/.env.{environment}.local - Environment-specific local overrides (gitignored)
374
     *
375
     * This mirrors the standard Symfony environment loading but for app-specific configuration.
376
     */
377
    private function loadAppEnvironmentFiles(): void
378
    {
379
        $appDir = $this->getProjectDir() . '/' . $this->apps . '/' . $this->id;
8✔
380
        $infrastructureDir = $this->getProjectDir() . '/' . $this->infrastructure;
8✔
381
        $dotenv = new Dotenv();
8✔
382

383
        $envFiles = [
8✔
384
            $infrastructureDir . '/.env',
8✔
385
            $infrastructureDir . '/.env.local',
8✔
386
            $infrastructureDir . '/.env.' . $this->environment,
8✔
387
            $infrastructureDir . '/.env.' . $this->environment . '.local',
8✔
388
            $appDir . '/.env',
8✔
389
            $appDir . '/.env.local',
8✔
390
            $appDir . '/.env.' . $this->environment,
8✔
391
            $appDir . '/.env.' . $this->environment . '.local',
8✔
392
        ];
8✔
393

394
        foreach ($envFiles as $envFile) {
8✔
395
            if (is_file($envFile)) {
8✔
396
                $dotenv->load($envFile);
1✔
397
            }
398
        }
399
    }
400
}
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