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
![]() |
|||
256 | } catch (\Exception $e) { |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||
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; |
|
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
|
|||
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
|
|||
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
|
|||
336 | } catch (\Exception $e) { |
||
0 ignored issues
–
show
Coding Style
Comprehensibility
introduced
by
|
|||
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); |
|
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()); |
|
490 | } |
||
491 | |||
492 | 3 | $legacyFile = \dirname($dir . $file) . '.legacy'; |
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
493 | 3 | if (file_exists($legacyFile)) { |
|
494 | @unlink($legacyFile); |
||
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); |
|
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 |