Kernel   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 191
c 2
b 0
f 0
dl 0
loc 410
rs 4.08
wmc 59

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getCacheHash() 0 18 3
A getProjectDir() 0 25 6
A reboot() 0 14 3
A getConnection() 0 9 2
A getCacheDir() 0 8 1
A registerBundles() 0 16 4
A __construct() 0 20 1
A configureRoutes() 0 12 1
A getPluginLoader() 0 3 1
A shutdown() 0 12 3
A getKernelParameters() 0 34 2
A initializeDatabaseConnectionVariables() 0 29 6
A configureContainer() 0 11 1
C boot() 0 57 16
A addBundleOverwrites() 0 5 3
A addFallbackRoute() 0 9 1
A addApiRoutes() 0 3 1
A dumpContainer() 0 21 1
A addBundleRoutes() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like Kernel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Kernel, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core;
4
5
use Doctrine\DBAL\Connection;
6
use Shopware\Core\DevOps\Environment\EnvironmentHelper;
7
use Shopware\Core\Framework\Adapter\Database\MySQLFactory;
8
use Shopware\Core\Framework\Api\Controller\FallbackController;
9
use Shopware\Core\Framework\Log\Package;
10
use Shopware\Core\Framework\Plugin\KernelPluginLoader\KernelPluginLoader;
11
use Shopware\Core\Framework\Util\VersionParser;
12
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
13
use Symfony\Component\Config\ConfigCache;
14
use Symfony\Component\Config\Loader\LoaderInterface;
15
use Symfony\Component\DependencyInjection\ContainerBuilder;
16
use Symfony\Component\DependencyInjection\ContainerInterface;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpKernel\Bundle\Bundle;
19
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
20
use Symfony\Component\HttpKernel\Kernel as HttpKernel;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Shopware\Core\HttpKernel. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
22
use Symfony\Component\Routing\Route;
23
24
#[Package('core')]
25
class Kernel extends HttpKernel
26
{
27
    use MicroKernelTrait;
0 ignored issues
show
introduced by
The trait Symfony\Bundle\Framework...Kernel\MicroKernelTrait requires some properties which are not provided by Shopware\Core\Kernel: $instanceof, $name
Loading history...
28
29
    final public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
30
31
    /**
32
     * @var string Fallback version if nothing is provided via kernel constructor
33
     */
34
    final public const SHOPWARE_FALLBACK_VERSION = '6.5.9999999.9999999-dev';
35
36
    /**
37
     * @var Connection|null
38
     */
39
    protected static $connection;
40
41
    /**
42
     * @var KernelPluginLoader
43
     */
44
    protected $pluginLoader;
45
46
    /**
47
     * @var string
48
     */
49
    protected $shopwareVersion;
50
51
    /**
52
     * @var string|null
53
     */
54
    protected $shopwareVersionRevision;
55
56
    /**
57
     * @var string|null
58
     */
59
    protected $projectDir;
60
61
    private bool $rebooting = false;
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function __construct(
67
        string $environment,
68
        bool $debug,
69
        KernelPluginLoader $pluginLoader,
70
        private string $cacheId,
71
        ?string $version = self::SHOPWARE_FALLBACK_VERSION,
72
        ?Connection $connection = null,
73
        ?string $projectDir = null
74
    ) {
75
        date_default_timezone_set('UTC');
76
77
        parent::__construct($environment, $debug);
78
        self::$connection = $connection;
79
80
        $this->pluginLoader = $pluginLoader;
81
82
        $version = VersionParser::parseShopwareVersion($version);
83
        $this->shopwareVersion = $version['version'];
84
        $this->shopwareVersionRevision = $version['revision'];
85
        $this->projectDir = $projectDir;
86
    }
87
88
    /**
89
     * @return iterable<BundleInterface>
90
     */
91
    public function registerBundles(): iterable
92
    {
93
        /** @var array<class-string<Bundle>, array<string, bool>> $bundles */
94
        $bundles = require $this->getProjectDir() . '/config/bundles.php';
95
        $instanciatedBundleNames = [];
96
97
        foreach ($bundles as $class => $envs) {
98
            if (isset($envs['all']) || isset($envs[$this->environment])) {
99
                $bundle = new $class();
100
                $instanciatedBundleNames[] = $bundle->getName();
101
102
                yield $bundle;
103
            }
104
        }
105
106
        yield from $this->pluginLoader->getBundles($this->getKernelParameters(), $instanciatedBundleNames);
107
    }
108
109
    public function getProjectDir(): string
110
    {
111
        if ($this->projectDir === null) {
112
            if ($dir = $_ENV['PROJECT_ROOT'] ?? $_SERVER['PROJECT_ROOT'] ?? false) {
113
                return $this->projectDir = $dir;
114
            }
115
116
            $r = new \ReflectionObject($this);
117
118
            $dir = (string) $r->getFileName();
119
            if (!file_exists($dir)) {
120
                throw new \LogicException(sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name));
121
            }
122
123
            $dir = $rootDir = \dirname($dir);
124
            while (!file_exists($dir . '/vendor')) {
125
                if ($dir === \dirname($dir)) {
126
                    return $this->projectDir = $rootDir;
127
                }
128
                $dir = \dirname($dir);
129
            }
130
            $this->projectDir = $dir;
131
        }
132
133
        return $this->projectDir;
134
    }
135
136
    public function boot(): void
137
    {
138
        if ($this->booted === true) {
139
            if ($this->debug) {
140
                $this->startTime = microtime(true);
141
            }
142
143
            return;
144
        }
145
146
        if ($this->debug) {
147
            $this->startTime = microtime(true);
148
        }
149
150
        if ($this->debug && !EnvironmentHelper::hasVariable('SHELL_VERBOSITY')) {
151
            putenv('SHELL_VERBOSITY=3');
152
            $_ENV['SHELL_VERBOSITY'] = 3;
153
            $_SERVER['SHELL_VERBOSITY'] = 3;
154
        }
155
156
        try {
157
            $this->pluginLoader->initializePlugins($this->getProjectDir());
158
        } catch (\Throwable $e) {
159
            if (\defined('\STDERR')) {
160
                fwrite(\STDERR, 'Warning: Failed to load plugins. Message: ' . $e->getMessage() . \PHP_EOL);
161
            }
162
        }
163
164
        // init bundles
165
        $this->initializeBundles();
166
167
        // init container
168
        $this->initializeContainer();
169
170
        // Taken from \Symfony\Component\HttpKernel\Kernel::preBoot()
171
        /** @var ContainerInterface $container */
172
        $container = $this->container;
173
174
        if ($container->hasParameter('kernel.trusted_hosts') && $trustedHosts = $container->getParameter('kernel.trusted_hosts')) {
175
            Request::setTrustedHosts($trustedHosts);
176
        }
177
178
        if ($container->hasParameter('kernel.trusted_proxies') && $container->hasParameter('kernel.trusted_headers') && $trustedProxies = $container->getParameter('kernel.trusted_proxies')) {
179
            \assert(\is_string($trustedProxies) || \is_array($trustedProxies));
180
            $trustedHeaderSet = $container->getParameter('kernel.trusted_headers');
181
            \assert(\is_int($trustedHeaderSet));
182
            Request::setTrustedProxies(\is_array($trustedProxies) ? $trustedProxies : array_map('trim', explode(',', $trustedProxies)), $trustedHeaderSet);
183
        }
184
185
        foreach ($this->getBundles() as $bundle) {
186
            $bundle->setContainer($this->container);
187
            $bundle->boot();
188
        }
189
190
        $this->initializeDatabaseConnectionVariables();
191
192
        $this->booted = true;
193
    }
194
195
    public static function getConnection(): Connection
196
    {
197
        if (self::$connection) {
198
            return self::$connection;
199
        }
200
201
        self::$connection = MySQLFactory::create();
202
203
        return self::$connection;
204
    }
205
206
    public function getCacheDir(): string
207
    {
208
        return sprintf(
209
            '%s/var/cache/%s_h%s%s',
210
            $this->getProjectDir(),
211
            $this->getEnvironment(),
212
            $this->getCacheHash(),
213
            EnvironmentHelper::getVariable('TEST_TOKEN') ?? ''
214
        );
215
    }
216
217
    public function getPluginLoader(): KernelPluginLoader
218
    {
219
        return $this->pluginLoader;
220
    }
221
222
    public function shutdown(): void
223
    {
224
        if (!$this->booted) {
225
            return;
226
        }
227
228
        // keep connection when rebooting
229
        if (!$this->rebooting) {
230
            self::$connection = null;
231
        }
232
233
        parent::shutdown();
234
    }
235
236
    public function reboot(?string $warmupDir, ?KernelPluginLoader $pluginLoader = null, ?string $cacheId = null): void
237
    {
238
        $this->rebooting = true;
239
240
        try {
241
            if ($pluginLoader) {
242
                $this->pluginLoader = $pluginLoader;
243
            }
244
            if ($cacheId) {
245
                $this->cacheId = $cacheId;
246
            }
247
            parent::reboot($warmupDir);
248
        } finally {
249
            $this->rebooting = false;
250
        }
251
    }
252
253
    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
254
    {
255
        $container->setParameter('.container.dumper.inline_class_loader', $this->environment !== 'test');
256
        $container->setParameter('.container.dumper.inline_factories', $this->environment !== 'test');
257
258
        $confDir = $this->getProjectDir() . '/config';
259
260
        $loader->load($confDir . '/{packages}/*' . self::CONFIG_EXTS, 'glob');
261
        $loader->load($confDir . '/{packages}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, 'glob');
262
        $loader->load($confDir . '/{services}' . self::CONFIG_EXTS, 'glob');
263
        $loader->load($confDir . '/{services}_' . $this->environment . self::CONFIG_EXTS, 'glob');
264
    }
265
266
    protected function configureRoutes(RoutingConfigurator $routes): void
267
    {
268
        $confDir = $this->getProjectDir() . '/config';
269
270
        $routes->import($confDir . '/{routes}/*' . self::CONFIG_EXTS, 'glob');
271
        $routes->import($confDir . '/{routes}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, 'glob');
272
        $routes->import($confDir . '/{routes}' . self::CONFIG_EXTS, 'glob');
273
274
        $this->addBundleRoutes($routes);
275
        $this->addApiRoutes($routes);
276
        $this->addBundleOverwrites($routes);
277
        $this->addFallbackRoute($routes);
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     *
283
     * @return array<string, mixed>
284
     */
285
    protected function getKernelParameters(): array
286
    {
287
        $parameters = parent::getKernelParameters();
288
289
        $activePluginMeta = [];
290
291
        foreach ($this->pluginLoader->getPluginInstances()->getActives() as $plugin) {
292
            $class = $plugin::class;
293
            $activePluginMeta[$class] = [
294
                'name' => $plugin->getName(),
295
                'path' => $plugin->getPath(),
296
                'class' => $class,
297
            ];
298
        }
299
300
        $pluginDir = $this->pluginLoader->getPluginDir($this->getProjectDir());
301
302
        $coreDir = \dirname((string) (new \ReflectionClass(self::class))->getFileName());
303
304
        return array_merge(
305
            $parameters,
306
            [
307
                'kernel.cache.hash' => $this->getCacheHash(),
308
                'kernel.shopware_version' => $this->shopwareVersion,
309
                'kernel.shopware_version_revision' => $this->shopwareVersionRevision,
310
                'kernel.shopware_core_dir' => $coreDir,
311
                'kernel.plugin_dir' => $pluginDir,
312
                'kernel.app_dir' => rtrim($this->getProjectDir(), '/') . '/custom/apps',
313
                'kernel.active_plugins' => $activePluginMeta,
314
                'kernel.plugin_infos' => $this->pluginLoader->getPluginInfos(),
315
                'kernel.supported_api_versions' => [2, 3, 4],
316
                'defaults_bool_true' => true,
317
                'defaults_bool_false' => false,
318
                'default_whitespace' => ' ',
319
            ]
320
        );
321
    }
322
323
    protected function getCacheHash(): string
324
    {
325
        $plugins = [];
326
        foreach ($this->pluginLoader->getPluginInfos() as $plugin) {
327
            if ($plugin['active'] === false) {
328
                continue;
329
            }
330
            $plugins[$plugin['name']] = $plugin['version'];
331
        }
332
333
        $pluginHash = md5((string) json_encode($plugins, \JSON_THROW_ON_ERROR));
334
335
        return md5((string) \json_encode([
336
            $this->cacheId,
337
            substr((string) $this->shopwareVersionRevision, 0, 8),
338
            substr($pluginHash, 0, 8),
339
            EnvironmentHelper::getVariable('DATABASE_URL', ''),
340
        ], \JSON_THROW_ON_ERROR));
341
    }
342
343
    protected function initializeDatabaseConnectionVariables(): void
344
    {
345
        $shopwareSkipConnectionVariables = EnvironmentHelper::getVariable('SHOPWARE_SKIP_CONNECTION_VARIABLES', false);
346
347
        if ($shopwareSkipConnectionVariables) {
348
            return;
349
        }
350
351
        $connection = self::getConnection();
352
353
        try {
354
            $setSessionVariables = (bool) EnvironmentHelper::getVariable('SQL_SET_DEFAULT_SESSION_VARIABLES', true);
355
            $connectionVariables = [];
356
357
            $timeZoneSupportEnabled = (bool) EnvironmentHelper::getVariable('SHOPWARE_DBAL_TIMEZONE_SUPPORT_ENABLED', false);
358
            if ($timeZoneSupportEnabled) {
359
                $connectionVariables[] = 'SET @@session.time_zone = "UTC"';
360
            }
361
362
            if ($setSessionVariables) {
363
                $connectionVariables[] = 'SET @@group_concat_max_len = CAST(IF(@@group_concat_max_len > 320000, @@group_concat_max_len, 320000) AS UNSIGNED)';
364
                $connectionVariables[] = 'SET sql_mode=(SELECT REPLACE(@@sql_mode,\'ONLY_FULL_GROUP_BY\',\'\'))';
365
            }
366
367
            if (empty($connectionVariables)) {
368
                return;
369
            }
370
            $connection->executeQuery(implode(';', $connectionVariables));
371
        } catch (\Throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
372
        }
373
    }
374
375
    /**
376
     * Dumps the preload file to an always known location outside the generated cache folder name
377
     */
378
    protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, string $class, string $baseClass): void
379
    {
380
        parent::dumpContainer($cache, $container, $class, $baseClass);
381
        $cacheDir = $this->getCacheDir();
382
        $cacheName = basename($cacheDir);
383
        $fileName = substr(basename($cache->getPath()), 0, -3) . 'preload.php';
384
385
        file_put_contents(\dirname($cacheDir) . '/CACHEDIR.TAG', 'Signature: 8a477f597d28d172789f06886806bc55');
386
387
        $preloadFile = \dirname($cacheDir) . '/opcache-preload.php';
388
389
        $loader = <<<PHP
390
<?php
391
392
require_once __DIR__ . '/#CACHE_PATH#';
393
PHP;
394
395
        file_put_contents($preloadFile, str_replace(
396
            ['#CACHE_PATH#'],
397
            [$cacheName . '/' . $fileName],
398
            $loader
399
        ));
400
    }
401
402
    private function addApiRoutes(RoutingConfigurator $routes): void
403
    {
404
        $routes->import('.', 'api');
405
    }
406
407
    private function addBundleRoutes(RoutingConfigurator $routes): void
408
    {
409
        foreach ($this->getBundles() as $bundle) {
410
            if ($bundle instanceof Framework\Bundle) {
411
                $bundle->configureRoutes($routes, $this->environment);
412
            }
413
        }
414
    }
415
416
    private function addBundleOverwrites(RoutingConfigurator $routes): void
417
    {
418
        foreach ($this->getBundles() as $bundle) {
419
            if ($bundle instanceof Framework\Bundle) {
420
                $bundle->configureRouteOverwrites($routes, $this->environment);
421
            }
422
        }
423
    }
424
425
    private function addFallbackRoute(RoutingConfigurator $routes): void
426
    {
427
        // detail routes
428
        $route = new Route('/');
429
        $route->setMethods(['GET']);
430
        $route->setDefault('_controller', FallbackController::class . '::rootFallback');
431
        $route->setDefault(PlatformRequest::ATTRIBUTE_ROUTE_SCOPE, ['storefront']);
432
433
        $routes->add('root.fallback', $route->getPath());
434
    }
435
}
436