422 lines
14 KiB
PHP
422 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace KTXC;
|
|
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Http\Response\JsonResponse;
|
|
use KTXC\Http\Response\RedirectResponse;
|
|
use KTXC\Http\Response\Response;
|
|
use KTXC\Injection\Builder;
|
|
use KTXC\Injection\Container;
|
|
use Psr\Container\ContainerInterface;
|
|
use KTXC\Module\ModuleManager;
|
|
use Psr\Log\LoggerInterface;
|
|
use KTXC\Logger\FileLogger;
|
|
use KTXC\Routing\Router;
|
|
use KTXC\Routing\Route;
|
|
use KTXC\Service\SecurityService;
|
|
use KTXC\Service\FirewallService;
|
|
use KTXF\Event\EventBus;
|
|
use KTXF\Cache\EphemeralCacheInterface;
|
|
use KTXF\Cache\PersistentCacheInterface;
|
|
use KTXF\Cache\BlobCacheInterface;
|
|
use KTXF\Cache\Store\FileEphemeralCache;
|
|
use KTXF\Cache\Store\FilePersistentCache;
|
|
use KTXF\Cache\Store\FileBlobCache;
|
|
use KTXM\MailManager\Queue\MailQueue;
|
|
use KTXM\MailManager\Queue\MailQueueFile;
|
|
|
|
class Kernel
|
|
{
|
|
public const VERSION = '1.0.0';
|
|
public const VERSION_ID = 10000;
|
|
public const MAJOR_VERSION = 1;
|
|
public const MINOR_VERSION = 0;
|
|
public const RELEASE_VERSION = 0;
|
|
public const EXTRA_VERSION = '';
|
|
|
|
protected bool $initialized = false;
|
|
protected bool $booted = false;
|
|
protected ?float $startTime = null;
|
|
protected ?ContainerInterface $container = null;
|
|
|
|
private string $projectDir;
|
|
|
|
public function __construct(
|
|
protected string $environment = 'prod',
|
|
protected bool $debug = false,
|
|
) {
|
|
if (!$environment) {
|
|
throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this)));
|
|
}
|
|
}
|
|
|
|
public function __clone()
|
|
{
|
|
$this->initialized = false;
|
|
$this->booted = false;
|
|
$this->container = null;
|
|
}
|
|
|
|
private function initialize(): void
|
|
{
|
|
if ($this->debug) {
|
|
$this->startTime = microtime(true);
|
|
}
|
|
if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) {
|
|
if (\function_exists('putenv')) {
|
|
putenv('SHELL_VERBOSITY=3');
|
|
}
|
|
$_ENV['SHELL_VERBOSITY'] = 3;
|
|
$_SERVER['SHELL_VERBOSITY'] = 3;
|
|
}
|
|
|
|
$container = $this->initializeContainer();
|
|
|
|
$this->container = $container;
|
|
$this->initialized = true;
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
if (!$this->initialized) {
|
|
$this->initialize();
|
|
}
|
|
|
|
if (!$this->booted) {
|
|
/** @var ModuleManager $moduleManager */
|
|
$moduleManager = $this->container->get(ModuleManager::class);
|
|
$moduleManager->modulesBoot();
|
|
$this->booted = true;
|
|
}
|
|
}
|
|
|
|
public function reboot(): void
|
|
{
|
|
$this->shutdown();
|
|
$this->boot();
|
|
}
|
|
|
|
public function shutdown(): void
|
|
{
|
|
if (false === $this->initialized) {
|
|
return;
|
|
}
|
|
|
|
$this->initialized = false;
|
|
$this->booted = false;
|
|
$this->container = null;
|
|
}
|
|
|
|
public function handle(Request $request): Response
|
|
{
|
|
if (!$this->booted) {
|
|
$this->boot();
|
|
}
|
|
|
|
/** @var SessionTenant $sessionTenant */
|
|
$sessionTenant = $this->container->get(SessionTenant::class);
|
|
$sessionTenant->configure($request->getHost());
|
|
if (!$sessionTenant->configured() && !$sessionTenant->enabled()) {
|
|
return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED);
|
|
}
|
|
|
|
/** @var FirewallService $firewall */
|
|
$firewall = $this->container->get(FirewallService::class);
|
|
if (!$firewall->authorized($request)) {
|
|
return new Response(Response::$statusTexts[Response::HTTP_FORBIDDEN], Response::HTTP_FORBIDDEN);
|
|
}
|
|
|
|
/** @var Router $router */
|
|
$router = $this->container->get(Router::class);
|
|
if ($router) {
|
|
$match = $router->match($request);
|
|
if ($match instanceof Route) {
|
|
/** @var SecurityService $securityService */
|
|
$securityService = $this->container->get(SecurityService::class);
|
|
$identity = $securityService->authenticate($request);
|
|
|
|
if ($match->authenticated && $identity === null) {
|
|
return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED);
|
|
}
|
|
|
|
if ($identity) {
|
|
/** @var SessionIdentity $sessionIdentity */
|
|
$sessionIdentity = $this->container->get(SessionIdentity::class);
|
|
$sessionIdentity->initialize($identity, true);
|
|
}
|
|
|
|
$response = $router->dispatch($match, $request);
|
|
if ($response instanceof Response) {
|
|
return $response;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Response(Response::$statusTexts[Response::HTTP_NOT_FOUND], Response::HTTP_NOT_FOUND);
|
|
}
|
|
|
|
/**
|
|
* Process deferred events at the end of the request
|
|
*/
|
|
public function processEvents(): void
|
|
{
|
|
try {
|
|
if ($this->container && $this->container->has(EventBus::class)) {
|
|
/** @var EventBus $eventBus */
|
|
$eventBus = $this->container->get(EventBus::class);
|
|
$eventBus->processDeferred();
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('Event processing error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the kernel parameters.
|
|
*
|
|
* @return array<string, array|bool|string|int|float|\UnitEnum|null>
|
|
*/
|
|
protected function parameters(): array
|
|
{
|
|
return [
|
|
'kernel.project_dir' => realpath($this->folderRoot()) ?: $this->folderRoot(),
|
|
'kernel.environment' => $this->environment,
|
|
'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%',
|
|
'kernel.runtime_mode' => '%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%',
|
|
'kernel.runtime_mode.web' => '%env(bool:default::key:web:default:kernel.runtime_mode:)%',
|
|
'kernel.runtime_mode.cli' => '%env(not:default:kernel.runtime_mode.web:)%',
|
|
'kernel.runtime_mode.worker' => '%env(bool:default::key:worker:default:kernel.runtime_mode:)%',
|
|
'kernel.debug' => $this->debug,
|
|
'kernel.build_dir' => realpath($this->getBuildDir()) ?: $this->getBuildDir(),
|
|
'kernel.cache_dir' => realpath($this->getCacheDir()) ?: $this->getCacheDir(),
|
|
'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(),
|
|
'kernel.charset' => $this->getCharset(),
|
|
];
|
|
}
|
|
|
|
public function environment(): string
|
|
{
|
|
return $this->environment;
|
|
}
|
|
|
|
public function debug(): bool
|
|
{
|
|
return $this->debug;
|
|
}
|
|
|
|
public function container(): ContainerInterface
|
|
{
|
|
if (!$this->container) {
|
|
throw new \LogicException('Cannot retrieve the container from a non-booted kernel.');
|
|
}
|
|
|
|
return $this->container;
|
|
}
|
|
|
|
public function getStartTime(): float
|
|
{
|
|
return $this->debug && null !== $this->startTime ? $this->startTime : -\INF;
|
|
}
|
|
|
|
/**
|
|
* Gets the application root dir (path of the project's composer file).
|
|
*/
|
|
public function folderRoot(): string
|
|
{
|
|
if (!isset($this->projectDir)) {
|
|
$r = new \ReflectionObject($this);
|
|
|
|
if (!is_file($dir = $r->getFileName())) {
|
|
throw new \LogicException(\sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name));
|
|
}
|
|
|
|
$dir = $rootDir = \dirname($dir);
|
|
while (!is_file($dir.'/composer.json')) {
|
|
if ($dir === \dirname($dir)) {
|
|
return $this->projectDir = $rootDir;
|
|
}
|
|
$dir = \dirname($dir);
|
|
}
|
|
$this->projectDir = $dir;
|
|
}
|
|
|
|
return $this->projectDir;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the path to the configuration directory.
|
|
*/
|
|
private function getConfigDir(): string
|
|
{
|
|
return $this->folderRoot().'/config';
|
|
}
|
|
|
|
public function getCacheDir(): string
|
|
{
|
|
return $this->folderRoot().'/var/cache/'.$this->environment;
|
|
}
|
|
|
|
public function getBuildDir(): string
|
|
{
|
|
// Returns $this->getCacheDir() for backward compatibility
|
|
return $this->getCacheDir();
|
|
}
|
|
|
|
public function getLogDir(): string
|
|
{
|
|
return $this->folderRoot().'/var/log';
|
|
}
|
|
|
|
public function getCharset(): string
|
|
{
|
|
return 'UTF-8';
|
|
}
|
|
|
|
/**
|
|
* Gets a new container builder instance used to build the service container.
|
|
*/
|
|
protected function containerBuilder(): Builder
|
|
{
|
|
return new Builder(Container::class);
|
|
}
|
|
|
|
/**
|
|
* Initializes the service container
|
|
*/
|
|
protected function initializeContainer(): Container
|
|
{
|
|
$container = $this->buildContainer();
|
|
$container->set('kernel', $this);
|
|
|
|
return $container;
|
|
}
|
|
|
|
/**
|
|
* Builds the service container.
|
|
*
|
|
* @throws \RuntimeException
|
|
*/
|
|
protected function buildContainer(): Container
|
|
{
|
|
$builder = $this->containerBuilder();
|
|
$builder->useAutowiring(true);
|
|
$builder->useAttributes(true);
|
|
$builder->addDefinitions($this->parameters());
|
|
$this->configureContainer($builder);
|
|
|
|
return $builder->build();
|
|
}
|
|
|
|
protected function configureContainer(Builder $builder): void
|
|
{
|
|
$builder->addDefinitions($this->getConfigDir() . '/system.php');
|
|
// Service definitions
|
|
$logDir = $this->getLogDir();
|
|
$projectDir = $this->folderRoot();
|
|
$builder->addDefinitions([
|
|
LoggerInterface::class => function() use ($logDir) {
|
|
return new FileLogger($logDir);
|
|
},
|
|
// EventBus as singleton for consistent event handling
|
|
EventBus::class => \DI\create(EventBus::class),
|
|
// Ephemeral Cache - for short-lived data (sessions, rate limits, challenges)
|
|
EphemeralCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
|
|
$storeType = $c->has('cache.ephemeral') ? $c->get('cache.ephemeral') : 'file';
|
|
|
|
$storeMap = [
|
|
'file' => FileEphemeralCache::class,
|
|
// 'redis' => RedisEphemeralCache::class,
|
|
];
|
|
|
|
$storeClass = $storeMap[$storeType] ?? $storeType;
|
|
|
|
if (!class_exists($storeClass)) {
|
|
throw new \RuntimeException("Ephemeral cache store not found: {$storeClass}");
|
|
}
|
|
|
|
$cache = new $storeClass($projectDir);
|
|
|
|
// Set tenant/user context if available
|
|
if ($c->has(SessionTenant::class)) {
|
|
$tenant = $c->get(SessionTenant::class);
|
|
$cache->setTenantContext($tenant->identifier());
|
|
}
|
|
if ($c->has(SessionIdentity::class)) {
|
|
$identity = $c->get(SessionIdentity::class);
|
|
$cache->setUserContext($identity->identifier());
|
|
}
|
|
|
|
return $cache;
|
|
},
|
|
// Persistent Cache - for long-lived data (routes, modules, compiled configs)
|
|
PersistentCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
|
|
$storeType = $c->has('cache.persistent') ? $c->get('cache.persistent') : 'file';
|
|
|
|
$storeMap = [
|
|
'file' => FilePersistentCache::class,
|
|
// 'database' => DatabasePersistentCache::class,
|
|
];
|
|
|
|
$storeClass = $storeMap[$storeType] ?? $storeType;
|
|
|
|
if (!class_exists($storeClass)) {
|
|
throw new \RuntimeException("Persistent cache store not found: {$storeClass}");
|
|
}
|
|
|
|
$cache = new $storeClass($projectDir);
|
|
|
|
// Set tenant/user context if available
|
|
if ($c->has(SessionTenant::class)) {
|
|
$tenant = $c->get(SessionTenant::class);
|
|
$cache->setTenantContext($tenant->identifier());
|
|
}
|
|
if ($c->has(SessionIdentity::class)) {
|
|
$identity = $c->get(SessionIdentity::class);
|
|
$cache->setUserContext($identity->identifier());
|
|
}
|
|
|
|
return $cache;
|
|
},
|
|
// Blob Cache - for binary/media data (previews, thumbnails)
|
|
BlobCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
|
|
$storeType = $c->has('cache.blob') ? $c->get('cache.blob') : 'file';
|
|
|
|
$storeMap = [
|
|
'file' => FileBlobCache::class,
|
|
// 's3' => S3BlobCache::class,
|
|
];
|
|
|
|
$storeClass = $storeMap[$storeType] ?? $storeType;
|
|
|
|
if (!class_exists($storeClass)) {
|
|
throw new \RuntimeException("Blob cache store not found: {$storeClass}");
|
|
}
|
|
|
|
$cache = new $storeClass($projectDir);
|
|
|
|
// Set tenant/user context if available
|
|
if ($c->has(SessionTenant::class)) {
|
|
$tenant = $c->get(SessionTenant::class);
|
|
$cache->setTenantContext($tenant->identifier());
|
|
}
|
|
if ($c->has(SessionIdentity::class)) {
|
|
$identity = $c->get(SessionIdentity::class);
|
|
$cache->setUserContext($identity->identifier());
|
|
}
|
|
|
|
return $cache;
|
|
},
|
|
]);
|
|
}
|
|
|
|
}
|