config = $config; if ($projectDir !== null) { $this->projectDir = $projectDir; } } 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; } // Create logger with config support $logDir = $this->config['log.directory'] ?? $this->getLogDir(); $logChannel = $this->config['log.channel'] ?? 'app'; $this->logger = new FileLogger($logDir, $logChannel); $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; }); // Handle uncaught exceptions set_exception_handler(function (\Throwable $exception) { $this->logger->error('Exception caught: ' . $exception->getMessage(), [ 'exception' => $exception, 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'trace' => $exception->getTraceAsString(), ]); if ($this->debug) { echo '
Uncaught Exception: ' . $exception . '
'; } else { echo 'An unexpected error occurred. Please try again later.'; } exit(1); }); // 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); if ($this->debug) { echo '
' . $message . '
'; } else { echo 'A fatal error occurred. Please try again later.'; } } }); } public function boot(): void { if (!$this->initialized) { $this->initialize(); } if (!$this->booted) { /** @var ModuleManager $moduleManager */ $moduleManager = $this->container->get(ModuleManager::class); $moduleManager->modulesBoot(); // Build middleware pipeline $this->pipeline = $this->buildMiddlewarePipeline(); $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(); } // Use middleware pipeline to handle the request return $this->pipeline->handle($request); } /** * Build the middleware pipeline */ protected function buildMiddlewarePipeline(): MiddlewarePipeline { $pipeline = new MiddlewarePipeline($this->container); // Register middleware in execution order $pipeline->pipe(TenantMiddleware::class); $pipeline->pipe(FirewallMiddleware::class); $pipeline->pipe(AuthenticationMiddleware::class); $pipeline->pipe(RouterMiddleware::class); return $pipeline; } /** * 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 */ 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 { return $this->getCacheDir(); } public function getLogDir(): string { return $this->folderRoot().'/var/log'; } public function getCharset(): string { return 'UTF-8'; } /** * 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->folderRoot(); $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), // Use the kernel's logger instance LoggerInterface::class => \DI\value($this->logger), // 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; }, ]); } }