config = $config; } public function __clone() { $this->initialized = false; $this->booted = false; $this->container = null; $this->runner = 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; } // Create logger from config (driver + level-filter; per-tenant wrapping applied later in DI) $this->logger = LoggerFactory::create( $this->config, $this->options->paths->project, ); $this->initializeErrorHandlers(); $container = $this->initializeContainer(); $this->container = $container; $this->initialized = true; } /** * Set up global error and exception handlers */ protected function initializeErrorHandlers(): void { // Convert PHP errors to exceptions set_error_handler(function ($errno, $errstr, $errfile, $errline) { // Don't throw exception if error reporting is turned off if (!(error_reporting() & $errno)) { return false; } $message = sprintf( "PHP Error [%d]: %s in %s:%d", $errno, $errstr, $errfile, $errline ); $this->logger->error($message, ['errno' => $errno, 'file' => $errfile, 'line' => $errline]); // Throw exception for fatal errors if ($errno === E_ERROR || $errno === E_CORE_ERROR || $errno === E_COMPILE_ERROR || $errno === E_USER_ERROR) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } return true; }); $this->errorHandlerInstalled = true; // Handle fatal errors register_shutdown_function(function () { $error = error_get_last(); if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) { $message = sprintf( "Fatal Error [%d]: %s in %s:%d", $error['type'], $error['message'], $error['file'], $error['line'] ); $this->logger->error($message, $error); } }); } public function boot(): void { if (!$this->initialized) { $this->initialize(); } if (!$this->booted) { /** @var ModuleManager $moduleManager */ $moduleManager = $this->container->get(ModuleManager::class); $moduleManager->modulesBoot(); $this->container ->get(EventListenerRegistry::class) ->freeze($this->container); $this->booted = true; } } public function executionRunner(): ExecutionRunnerInterface { return $this->runner ??= new ExecutionRunner($this); } public function reboot(): void { $this->shutdown(); $this->boot(); } public function shutdown(): void { if (false === $this->initialized) { return; } if ($this->activeScope !== null && !$this->activeScope->terminated()) { throw new \LogicException('Cannot shut down the kernel while an execution scope is active.'); } $this->initialized = false; $this->booted = false; $this->container = null; if ($this->errorHandlerInstalled) { restore_error_handler(); $this->errorHandlerInstalled = false; } } public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope { if (!$this->booted) { $this->boot(); } if ($this->activeScope !== null) { throw new \LogicException('The kernel already has an active execution scope.'); } $scope = new ExecutionScope( $descriptor, \KTXC\Application\Execution\ExecutionContext::fromDescriptor($descriptor), $this->container->get(TenantContext::class), $this->container->get(IdentityContext::class), ); $this->container ->get(DeferredEventProcessorInterface::class) ->beginExecution($descriptor->executionId); $this->activeScope = $scope; return $scope; } public function terminateExecution( ExecutionScope $scope, ExecutionOutcome $outcome, ): TerminationReport { if ($scope->terminated()) { return new TerminationReport(); } if ($this->activeScope !== $scope) { throw new \LogicException('Cannot terminate a scope that is not active.'); } $processed = 0; $remaining = 0; $deadlineExceeded = false; $limitExceeded = false; $listenerInvocations = 0; $eventLimitExceeded = false; $listenerInvocationLimitExceeded = false; $failures = []; try { if ($this->container && $this->container->has(DeferredEventProcessorInterface::class)) { $result = $this->container ->get(DeferredEventProcessorInterface::class) ->processDeferred($scope->descriptor->executionId); $processed = $result->processed; $remaining = $result->remaining; $deadlineExceeded = $result->deadlineExceeded; $limitExceeded = $result->limitExceeded; $listenerInvocations = $result->listenerInvocations; $eventLimitExceeded = $result->eventLimitExceeded; $listenerInvocationLimitExceeded = $result->listenerInvocationLimitExceeded; } } catch (\Throwable $e) { $failures[] = $e; try { $this->container ?->get(DeferredEventProcessorInterface::class) ->discardDeferred($scope->descriptor->executionId); } catch (\Throwable $discardError) { $failures[] = $discardError; } $this->logger?->error('Deferred event processing failed.', [ 'exception' => $e, 'execution_id' => $scope->descriptor->executionId, 'runtime' => $scope->descriptor->runtime->value, ]); } finally { if ($this->container !== null) { foreach ([ EphemeralCacheInterface::class, PersistentCacheInterface::class, BlobCacheInterface::class, ] as $cacheType) { $cache = $this->container->get($cacheType); $cache->setUserContext(null); $cache->setTenantContext(null); } } $scope->dispose(); $scope->markTerminated(); $this->activeScope = null; } return new TerminationReport( deferredProcessed: $processed, deferredRemaining: $remaining, failures: $failures, deadlineExceeded: $deadlineExceeded, limitExceeded: $limitExceeded, deferredListenerInvocations: $listenerInvocations, deferredEventLimitExceeded: $eventLimitExceeded, deferredListenerInvocationLimitExceeded: $listenerInvocationLimitExceeded, ); } /** * Returns the kernel parameters. * * @return array */ protected function parameters(): array { $projectDir = $this->options->paths->project; $cacheDir = $this->options->paths->cache($this->environment()); $logsDir = $this->options->paths->logs(); return [ 'kernel.project_dir' => realpath($projectDir) ?: $projectDir, '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($cacheDir) ?: $cacheDir, 'kernel.cache_dir' => realpath($cacheDir) ?: $cacheDir, 'kernel.logs_dir' => realpath($logsDir) ?: $logsDir, 'kernel.charset' => 'UTF-8', ]; } public function environment(): string { return $this->options->environment; } public function debug(): bool { return $this->options->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; } /** * 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 = new Builder(Container::class); $builder->useAutowiring(true); $builder->useAttributes(true); $builder->addDefinitions($this->parameters()); $builder->addDefinitions($this->config); $this->configureContainer($builder); return $builder->build(); } protected function configureContainer(Builder $builder): void { // Service definitions $projectDir = $this->options->paths->project; $moduleDir = $projectDir . '/modules'; $environment = $this->environment(); $builder->addDefinitions([ // Provide primitives for injection 'rootDir' => \DI\value($projectDir), 'moduleDir' => \DI\value($moduleDir), 'environment' => \DI\value($environment), // IMPORTANT: ensure Container::class resolves to the *current* container instance. // Without this alias, PHP-DI will happily autowire a new empty Container when asked Container::class => \DI\get(ContainerInterface::class), TenantContextInterface::class => \DI\get(TenantContext::class), IdentityContextInterface::class => \DI\get(IdentityContext::class), LoggerInterface::class => function (ContainerInterface $c) use ($projectDir) { $logConfig = $this->config['log'] ?? []; $logDir = $logConfig['path'] ?? ($projectDir . '/var/log'); $channel = $logConfig['channel'] ?? 'app'; $level = $logConfig['level'] ?? 'debug'; $perTenant = (bool) ($logConfig['per_tenant'] ?? false); return new TenantAwareLogger( $this->logger, $c->get(TenantContextInterface::class), $logDir, $channel, $level, $perTenant, ); }, EventDispatcherInterface::class => \DI\get(EventDispatcher::class), DeferredEventProcessorInterface::class => \DI\get(EventDispatcher::class), EventListenerRegistrarInterface::class => \DI\get(EventListenerRegistry::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(TenantContextInterface::class)) { $tenantContext = $c->get(TenantContextInterface::class); $cache->setTenantContext($tenantContext->identifier()); } if ($c->has(IdentityContextInterface::class)) { $identityContext = $c->get(IdentityContextInterface::class); $cache->setUserContext($identityContext->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(TenantContextInterface::class)) { $tenantContext = $c->get(TenantContextInterface::class); $cache->setTenantContext($tenantContext->identifier()); } if ($c->has(IdentityContextInterface::class)) { $identityContext = $c->get(IdentityContextInterface::class); $cache->setUserContext($identityContext->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(TenantContextInterface::class)) { $tenantContext = $c->get(TenantContextInterface::class); $cache->setTenantContext($tenantContext->identifier()); } if ($c->has(IdentityContextInterface::class)) { $identityContext = $c->get(IdentityContextInterface::class); $cache->setUserContext($identityContext->identifier()); } return $cache; }, ]); } }