MikroKernel::serialize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Thruster\MikroKernel;
6
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Server\RequestHandlerInterface;
10
use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator;
11
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
12
use Symfony\Component\Config\ConfigCache;
13
use Symfony\Component\Config\FileLocator;
14
use Symfony\Component\Config\Loader\DelegatingLoader;
15
use Symfony\Component\Config\Loader\LoaderResolver;
16
use Symfony\Component\Debug\DebugClassLoader;
17
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
18
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
19
use Symfony\Component\DependencyInjection\ContainerBuilder;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
22
use Symfony\Component\DependencyInjection\Loader\ClosureLoader;
23
use Symfony\Component\DependencyInjection\Loader\DirectoryLoader;
24
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
25
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
26
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
27
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
28
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
29
use Symfony\Component\Filesystem\Filesystem;
30
use Thruster\HttpFactory\HttpFactoryInterface;
31
use Thruster\HttpFactory\ZendDiactorosHttpFactory;
32
33
/**
34
 * Class MikroKernel.
35
 *
36
 * @author  Aurimas Niekis <[email protected]>
37
 */
38
abstract class MikroKernel implements MikroKernelInterface
39
{
40
    /** @var ContainerInterface */
41
    protected $container;
42
43
    /** @var string */
44
    protected $environment;
45
46
    /** @var bool */
47
    protected $debug;
48
49
    /** @var bool */
50
    protected $booted;
51
52
    /** @var int */
53
    protected $startTime;
54
55
    /** @var string */
56
    private $projectDir;
57
58
    /** @var string */
59
    private $warmupDir;
60
61
    /** @var RequestHandlerInterface */
62
    private $requestHandler;
63
64 24
    public function __construct(string $environment = 'dev', bool $debug = true)
65
    {
66 24
        $this->environment = $environment;
67 24
        $this->debug       = $debug;
68 24
        $this->booted      = false;
69 24
    }
70
71 3
    public function __clone()
72
    {
73 3
        $this->booted    = false;
74 3
        $this->container = null;
75 3
        $this->startTime = null;
76 3
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 12
    public function boot(): void
82
    {
83 12
        if ($this->booted) {
84 3
            return;
85
        }
86
87 12
        if ($this->debug) {
88 6
            $this->startTime = time();
89
        }
90
91 12
        if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) {
92 3
            putenv('SHELL_VERBOSITY=3');
93 3
            $_ENV['SHELL_VERBOSITY']    = 3;
94 3
            $_SERVER['SHELL_VERBOSITY'] = 3;
95
        }
96
97
        // init container
98 12
        $this->initializeContainer();
99
100 12
        $this->booted = true;
101 12
    }
102
103 6
    public function handle(ServerRequestInterface $request): ResponseInterface
104
    {
105 6
        $requestHandler = $this->getRequestHandler();
106
107 6
        if (null !== $requestHandler) {
108 3
            return $requestHandler->handle($request);
109
        }
110
111 3
        return $this->getHttpFactory()
112 3
            ->response()
113 3
            ->createResponse()
114 3
            ->withBody($this->getHttpFactory()->stream()->createStream('<h1>It Works!</h1>'));
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 6
    public function getEnvironment(): string
121
    {
122 6
        return $this->environment;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 6
    public function isDebug(): bool
129
    {
130 6
        return $this->debug;
131
    }
132
133
    /**
134
     * Gets the application root dir (path of the project's composer file).
135
     *
136
     * @return string The project root dir
137
     */
138
    public function getProjectDir(): string
139
    {
140
        if (null === $this->projectDir) {
141
            $r   = new \ReflectionObject($this);
142
            $dir = $rootDir = \dirname($r->getFileName());
143
            while (!file_exists($dir . '/composer.json')) {
144
                if ($dir === \dirname($dir)) {
145
                    return $this->projectDir = $rootDir;
146
                }
147
                $dir = \dirname($dir);
148
            }
149
            $this->projectDir = $dir;
150
        }
151
152
        return $this->projectDir;
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 9
    public function getContainer(): ?ContainerInterface
159
    {
160 9
        return $this->container;
161
    }
162
163 6
    public function getRequestHandler(): ?RequestHandlerInterface
164
    {
165 6
        if (null !== $this->requestHandler) {
166 3
            return $this->requestHandler;
167
        }
168
169 6
        if (false === $this->container->has(RequestHandlerInterface::class)) {
170 3
            return null;
171
        }
172
173 3
        $this->requestHandler = $this->container->get(RequestHandlerInterface::class);
174
175 3
        return $this->requestHandler;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function getStartTime(): int
182
    {
183
        return $this->debug ? $this->startTime : 0;
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189 6
    public function getCacheDir(): string
190
    {
191 6
        return $this->getProjectDir() . '/var/cache/' . $this->environment;
192
    }
193
194
    /**
195
     * Use this method to register compiler passes and manipulate the container during the building process.
196
     *
197
     * @param ContainerBuilder $container
198
     */
199
    protected function build(ContainerBuilder $container): void
200
    {
201
    }
202
203
    /**
204
     * Gets the container class.
205
     *
206
     * @return string The container class
207
     */
208 6
    protected function getContainerClass(): string
209
    {
210 6
        $class = \get_class($this);
211 6
        if ('c' === $class[0] && 0 === strpos($class, "class@anonymous\0")) {
212
            $class = \get_parent_class($class) . str_replace('.', '_', ContainerBuilder::hash($class));
213
        }
214
215 6
        return str_replace('\\', '_', $class) .
216 6
            ucfirst($this->environment) . ($this->debug ? 'Debug' : '') . 'Container';
217
    }
218
219
    /**
220
     * Gets the container's base class.
221
     *
222
     * All names except Container must be fully qualified.
223
     *
224
     * @return string
225
     */
226 3
    protected function getContainerBaseClass(): string
227
    {
228 3
        return 'Container';
229
    }
230
231
    /**
232
     * Initializes the service container.
233
     *
234
     * The cached version of the service container is used when fresh, otherwise the
235
     * container is built.
236
     */
237 6
    protected function initializeContainer(): void
238
    {
239 6
        $class        = $this->getContainerClass();
240 6
        $cacheDir     = $this->warmupDir ?: $this->getCacheDir();
241 6
        $cache        = new ConfigCache($cacheDir . '/' . $class . '.php', $this->debug);
242 6
        $oldContainer = null;
243
244 6
        if ($fresh = $cache->isFresh()) {
245
            // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors
246 3
            $errorLevel = error_reporting(\E_ALL ^ \E_WARNING);
247 3
            $fresh      = $oldContainer = false;
248
249
            try {
250 3
                if (file_exists($cache->getPath()) && \is_object($this->container = include $cache->getPath())) {
251 3
                    $this->container->set('kernel', $this);
252 3
                    $oldContainer = $this->container;
253 3
                    $fresh        = true;
254
                }
255
            } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
256
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
257 3
            } finally {
258 3
                error_reporting($errorLevel);
259
            }
260
        }
261
262 6
        if ($fresh) {
263 3
            return;
264
        }
265
266 3
        if ($this->debug) {
267 3
            $collectedLogs   = [];
268 3
            $previousHandler = \defined('PHPUNIT_COMPOSER_INSTALL');
269
            $previousHandler = $previousHandler ?: set_error_handler(function ($type, $message, $file, $line) use (
270
                &$collectedLogs,
271
                &$previousHandler
272
            ) {
273
                if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) {
274
                    /* @var callable $previousHandler */
275
                    return $previousHandler ? $previousHandler($type, $message, $file, $line) : false;
276
                }
277
278
                if (isset($collectedLogs[$message])) {
279
                    $collectedLogs[$message]['count']++;
280
281
                    return;
282
                }
283
284
                $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
285
                // Clean the trace by removing first frames added by the error handler itself.
286
                for ($i = 0; isset($backtrace[$i]); $i++) {
287
                    if ($backtrace[$i]['line'] ?? null === $line && $backtrace[$i]['file'] ?? null === $file) {
288
                        $backtrace = \array_slice($backtrace, 1 + $i);
289
                        break;
290
                    }
291
                }
292
                // Remove frames added by DebugClassLoader.
293
                for ($i = \count($backtrace) - 2; 0 < $i; $i--) {
294
                    if (DebugClassLoader::class === ($backtrace[$i]['class'] ?? null)) {
295
                        $backtrace = [$backtrace[$i + 1]];
296
                        break;
297
                    }
298
                }
299
300
                $collectedLogs[$message] = [
301
                    'type'    => $type,
302
                    'message' => $message,
303
                    'file'    => $file,
304
                    'line'    => $line,
305
                    'trace'   => [$backtrace[0]],
306
                    'count'   => 1,
307
                ];
308 3
            });
309
        }
310
311
        try {
312 3
            $container = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $container is dead and can be removed.
Loading history...
313 3
            $container = $this->buildContainer();
314 3
            $container->compile();
315 3
        } finally {
316 3
            if ($this->debug && true !== $previousHandler) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $previousHandler does not seem to be defined for all execution paths leading up to this point.
Loading history...
317
                restore_error_handler();
318
319
                file_put_contents(
320
                    $cacheDir . '/' . $class . 'Deprecations.log',
321
                    serialize(array_values($collectedLogs))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $collectedLogs does not seem to be defined for all execution paths leading up to this point.
Loading history...
322
                );
323
                file_put_contents(
324
                    $cacheDir . '/' . $class . 'Compiler.log',
325 3
                    null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''
326
                );
327
            }
328
        }
329
330 3
        if (null === $oldContainer && file_exists($cache->getPath())) {
331 3
            $errorLevel = error_reporting(\E_ALL ^ \E_WARNING);
332
333
            try {
334 3
                $oldContainer = include $cache->getPath();
335
            } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
336
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
337 3
            } finally {
338 3
                error_reporting($errorLevel);
339
            }
340
        }
341
342 3
        $oldContainer = \is_object($oldContainer) ? new \ReflectionClass($oldContainer) : false;
343
344 3
        $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());
345 3
        $this->container = require $cache->getPath();
346 3
        $this->container->set('kernel', $this);
347
348 3
        if ($oldContainer && \get_class($this->container) !== $oldContainer->name) {
349
            // Because concurrent requests might still be using them,
350
            // old container files are not removed immediately,
351
            // but on a next dump of the container.
352 3
            static $legacyContainers = [];
353 3
            $oldContainerDir         = \dirname($oldContainer->getFileName());
354
355 3
            $legacyContainers[$oldContainerDir . '.legacy'] = true;
356 3
            foreach (glob(\dirname($oldContainerDir) . \DIRECTORY_SEPARATOR . '*.legacy') as $legacyContainer) {
357 3
                if (!isset($legacyContainers[$legacyContainer]) && @unlink($legacyContainer)) {
358 3
                    (new Filesystem())->remove(substr($legacyContainer, 0, -7));
359
                }
360
            }
361
362 3
            touch($oldContainerDir . '.legacy');
363
        }
364
365 3
        if ($this->container->has('cache_warmer')) {
366
            $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'));
367
        }
368 3
    }
369
370
    /**
371
     * Returns the kernel parameters.
372
     *
373
     * @return array An array of kernel parameters
374
     */
375 3
    protected function getKernelParameters(): array
376
    {
377
        return [
378 3
            'kernel.project_dir'     => realpath($this->getProjectDir()) ?: $this->getProjectDir(),
379 3
            'kernel.environment'     => $this->environment,
380 3
            'kernel.debug'           => $this->debug,
381 3
            'kernel.cache_dir'       => realpath($cacheDir = $this->warmupDir ?: $this->getCacheDir()) ?: $cacheDir,
382 3
            'kernel.container_class' => $this->getContainerClass(),
383
        ];
384
    }
385
386
    /**
387
     * Builds the service container.
388
     *
389
     * @return ContainerBuilder The compiled service container
390
     *
391
     * @throws \RuntimeException
392
     */
393 3
    protected function buildContainer()
394
    {
395 3
        $dir = $this->warmupDir ?: $this->getCacheDir();
396 3
        if (!is_dir($dir)) {
397
            if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
398
                throw new \RuntimeException(sprintf("Unable to create the cache directory (%s)\n", $dir));
399
            }
400 3
        } elseif (!is_writable($dir)) {
401
            throw new \RuntimeException(sprintf("Unable to write in the cache directory (%s)\n", $dir));
402
        }
403
404 3
        $container = $this->getContainerBuilder();
405 3
        $container->addObjectResource($this);
406 3
        $this->build($container);
407
408 3
        $this->registerContainerConfiguration($this->getContainerLoader($container));
409
410 3
        return $container;
411
    }
412
413
    /**
414
     * Gets a new ContainerBuilder instance used to build the service container.
415
     *
416
     * @return ContainerBuilder
417
     */
418 3
    protected function getContainerBuilder(): ContainerBuilder
419
    {
420 3
        $container = new ContainerBuilder();
421 3
        $container->getParameterBag()->add($this->getKernelParameters());
422
423 3
        if ($this instanceof CompilerPassInterface) {
424
            $container->addCompilerPass($this, PassConfig::TYPE_BEFORE_OPTIMIZATION, -10000);
425
        }
426
427 3
        if (class_exists('ProxyManager\Configuration') &&
428 3
            class_exists('Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator')
429
        ) {
430 3
            $container->setProxyInstantiator(new RuntimeInstantiator());
431
        }
432
433 3
        $container->register(HttpFactoryInterface::class, $this->getHttpFactoryClass())
434 3
            ->setPublic(true);
435
436 3
        $container->setAlias('http_factory', HttpFactoryInterface::class);
437
438 3
        return $container;
439
    }
440
441 3
    public function getHttpFactory(): HttpFactoryInterface
442
    {
443 3
        return $this->container->get(HttpFactoryInterface::class);
444
    }
445
446 3
    public function getHttpFactoryClass(): string
447
    {
448 3
        return ZendDiactorosHttpFactory::class;
449
    }
450
451
    /**
452
     * Dumps the service container to PHP code in the cache.
453
     *
454
     * @param ConfigCache      $cache     The config cache
455
     * @param ContainerBuilder $container The service container
456
     * @param string           $class     The name of the class to generate
457
     * @param string           $baseClass The name of the container's base class
458
     */
459 3
    protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, $class, $baseClass): void
460
    {
461
        // cache the container
462 3
        $dumper = new PhpDumper($container);
463
464 3
        if (class_exists('ProxyManager\Configuration') &&
465 3
            class_exists('Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper')
466
        ) {
467 3
            $dumper->setProxyDumper(new ProxyDumper());
468
        }
469
470 3
        if ($container->hasParameter('kernel.container_build_time')) {
471
            $buildTime =  $container->getParameter('kernel.container_build_time');
472
        }
473
474 3
        $content = $dumper->dump([
475 3
            'class'      => $class,
476 3
            'base_class' => $baseClass,
477 3
            'file'       => $cache->getPath(),
478
            'as_files'   => true,
479 3
            'debug'      => $this->debug,
480 3
            'build_time' => $buildTime ?? time(),
481
        ]);
482
483 3
        $rootCode = array_pop($content);
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type string; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

483
        $rootCode = array_pop(/** @scrutinizer ignore-type */ $content);
Loading history...
484 3
        $dir      = \dirname($cache->getPath()) . '/';
485 3
        $fs       = new Filesystem();
486
487 3
        foreach ($content as $file => $code) {
488 3
            $fs->dumpFile($dir . $file, $code);
489 3
            @chmod($dir . $file, 0666 & ~umask());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

489
            /** @scrutinizer ignore-unhandled */ @chmod($dir . $file, 0666 & ~umask());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
490
        }
491
492 3
        $legacyFile = \dirname($dir . $file) . '.legacy';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $file seems to be defined by a foreach iteration on line 487. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
493 3
        if (file_exists($legacyFile)) {
494
            @unlink($legacyFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

494
            /** @scrutinizer ignore-unhandled */ @unlink($legacyFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
495
        }
496
497 3
        $cache->write($rootCode, $container->getResources());
498 3
    }
499
500
    /**
501
     * Returns a loader for the container.
502
     *
503
     * @param ContainerInterface $container
504
     *
505
     * @return DelegatingLoader The loader
506
     */
507 3
    protected function getContainerLoader(ContainerInterface $container)
508
    {
509 3
        $locator  = new FileLocator($this);
0 ignored issues
show
Bug introduced by
$this of type Thruster\MikroKernel\MikroKernel is incompatible with the type string|string[] expected by parameter $paths of Symfony\Component\Config...eLocator::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

509
        $locator  = new FileLocator(/** @scrutinizer ignore-type */ $this);
Loading history...
510 3
        $resolver = new LoaderResolver([
511 3
            new XmlFileLoader($container, $locator),
512 3
            new YamlFileLoader($container, $locator),
513 3
            new IniFileLoader($container, $locator),
514 3
            new PhpFileLoader($container, $locator),
515 3
            new GlobFileLoader($container, $locator),
516 3
            new DirectoryLoader($container, $locator),
517 3
            new ClosureLoader($container),
518
        ]);
519
520 3
        return new DelegatingLoader($resolver);
521
    }
522
523 3
    public function serialize()
524
    {
525 3
        return serialize([$this->environment, $this->debug]);
526
    }
527
528
    public function unserialize($data): void
529
    {
530
        [$environment, $debug] = unserialize($data, ['allowed_classes' => false]);
531
532
        $this->__construct($environment, $debug);
533
    }
534
}
535