rootDir = $this->resolveProjectRoot($rootDir); // Load configuration $this->config = $this->loadConfig(); // Determine environment and debug mode $environment = $environment ?? $this->config['environment'] ?? 'prod'; $debug = $debug ?? $this->config['debug'] ?? false; // Create kernel with configuration $this->kernel = new Kernel($environment, $debug, $this->config, $rootDir); // Register module autoloader for both HTTP and CLI contexts $moduleAutoloader = new ModuleAutoloader($this->moduleDir()); $moduleAutoloader->register(); } /** * Run the application - handle incoming request and send response */ public function runHttp(): void { try { $request = Request::createFromGlobals(); $response = $this->handle($request); $response->send(); $this->terminate(); } catch (\Throwable $e) { // Last resort error handling for kernel initialization failures error_log('Application error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); $content = $this->kernel->debug() ? '
' . htmlspecialchars((string) $e) . '
' : 'An error occurred. Please try again later.'; $response = new Response($content, Response::HTTP_INTERNAL_SERVER_ERROR, [ 'Content-Type' => 'text/html; charset=UTF-8', ]); $response->send(); exit(1); } } /** * Run as a console application (CLI runtime). */ public function runConsole(): int { $this->kernel()->boot(); $container = $this->container(); $console = new ConsoleApplication('Ktrix Console', Kernel::VERSION); /** @var ModuleManager $moduleManager */ $moduleManager = $container->get(ModuleManager::class); foreach ($moduleManager->list() as $module) { $instance = $module->instance(); if (!$instance instanceof ModuleConsoleInterface) { continue; } try { foreach ($instance->registerCI() as $commandClass) { if (!class_exists($commandClass)) { fwrite(STDERR, "Warning: Command class not found: {$commandClass}\n"); continue; } $this->registerLazyCommand($console, $container, $commandClass); } } catch (\Throwable $e) { fwrite(STDERR, "Warning: Failed to load commands from module {$module->handle()}: {$e->getMessage()}\n"); } } return $console->run(); } /** * Handle a request */ public function handle(Request $request): Response { return $this->kernel->handle($request); } /** * Terminate the application - process deferred events */ public function terminate(): void { $this->kernel->processEvents(); } /** * Get the kernel instance */ public function kernel(): Kernel { return $this->kernel; } /** * Get the container instance */ public function container(): ContainerInterface { return $this->kernel->container(); } /** * Get the application root directory */ public function rootDir(): string { return $this->rootDir; } /** * Get the modules directory */ public function moduleDir(): string { return $this->rootDir . '/modules'; } public function varDir(): string { return $this->rootDir . '/var'; } public function logDir(): string { return $this->varDir() . '/logs'; } /** * Get configuration value */ public function config(?string $key = null, mixed $default = null): mixed { if ($key === null) { return $this->config; } // Support dot notation: 'database.uri' $keys = explode('.', $key); $value = $this->config; foreach ($keys as $k) { if (!is_array($value) || !array_key_exists($k, $value)) { return $default; } $value = $value[$k]; } return $value; } /** * Get environment */ public function environment(): string { return $this->kernel->environment(); } /** * Check if debug mode is enabled */ public function debug(): bool { return $this->kernel->debug(); } /** * Load configuration from config directory */ protected function loadConfig(): array { $configFile = $this->rootDir . '/config/system.php'; if (!file_exists($configFile)) { error_log('Configuration file not found: ' . $configFile); return []; } $config = include $configFile; if (!is_array($config)) { throw new \RuntimeException('Configuration file must return an array'); } return $config; } /** * Resolve the project root directory. * * Some entrypoints may pass the public/ directory or another subdirectory. * We walk up the directory tree until we find composer.json. */ private function resolveProjectRoot(string $startDir): string { $dir = rtrim($startDir, '/'); if ($dir === '') { return $startDir; } // If startDir is a file path, use its directory. if (is_file($dir)) { $dir = dirname($dir); } $current = $dir; while (true) { if (is_file($current . '/composer.json')) { return $current; } $parent = dirname($current); if ($parent === $current) { // Reached filesystem root return $dir; } $current = $parent; } } /** * Set the Composer ClassLoader instance */ public static function setComposerLoader($loader): void { self::$composerLoader = $loader; } /** * Get the Composer ClassLoader instance */ public static function getComposerLoader() { return self::$composerLoader; } /** * Get the current Application instance */ public static function getInstance(): ?self { return self::$instance; } /** * Register a single command via lazy loading using its #[AsCommand] attribute. */ private function registerLazyCommand( ConsoleApplication $console, ContainerInterface $container, string $commandClass ): void { try { $ref = new \ReflectionClass($commandClass); $attrs = $ref->getAttributes(AsCommand::class); if (empty($attrs)) { fwrite(STDERR, "Warning: Command {$commandClass} missing #[AsCommand] attribute\n"); return; } $attr = $attrs[0]->newInstance(); $console->add(new LazyCommand( $attr->name, [], $attr->description ?? '', $attr->hidden ?? false, fn() => $container->get($commandClass) )); } catch (\Throwable $e) { fwrite(STDERR, "Warning: Failed to register command {$commandClass}: {$e->getMessage()}\n"); } } }