diff --git a/bin/console b/bin/console index 619d251..a4b912e 100755 --- a/bin/console +++ b/bin/console @@ -3,21 +3,12 @@ /** * Console Entry Point - * - * Bootstraps the application container and registers console commands - * from core and modules using lazy loading via Symfony Console. */ declare(strict_types=1); -use KTXC\Application; -use KTXC\Kernel; -use KTXC\Module\ModuleManager; -use KTXF\Module\ModuleConsoleInterface; -use Symfony\Component\Console\Application as ConsoleApplication; -use Symfony\Component\Console\Command\LazyCommand; +use KTXC\Server; -// Check dependencies if (!is_dir(dirname(__DIR__).'/vendor')) { fwrite(STDERR, "Dependencies are missing. Run 'composer install' first.\n"); exit(1); @@ -26,92 +17,12 @@ if (!is_dir(dirname(__DIR__).'/vendor')) { require_once dirname(__DIR__).'/vendor/autoload.php'; try { - // Bootstrap the application - $projectRoot = dirname(__DIR__); - $app = new Application($projectRoot); - - // Boot kernel to initialize container and modules - $app->kernel()->boot(); - - // Get the container - $container = $app->container(); - - // Create Symfony Console Application - $console = new ConsoleApplication('Ktrix Console', Kernel::VERSION); - - // Collect all command classes - $commandClasses = []; - - // Collect commands from modules - /** @var ModuleManager $moduleManager */ - $moduleManager = $container->get(ModuleManager::class); - - foreach ($moduleManager->list() as $module) { - $moduleInstance = $module->instance(); - - // Skip if module instance is not available - if ($moduleInstance === null) { - continue; - } - - // Check if module implements console command provider - if ($moduleInstance instanceof ModuleConsoleInterface) { - try { - $commands = $moduleInstance->registerCI(); - - foreach ($commands as $commandClass) { - if (!class_exists($commandClass)) { - fwrite(STDERR, "Warning: Command class not found: {$commandClass}\n"); - continue; - } - $commandClasses[] = $commandClass; - } - } catch (\Throwable $e) { - fwrite(STDERR, "Warning: Failed to load commands from module {$module->handle()}: {$e->getMessage()}\n"); - } - } - } - - // Register commands using lazy loading - foreach ($commandClasses as $commandClass) { - try { - // Use reflection to read #[AsCommand] attribute without instantiation - $reflection = new \ReflectionClass($commandClass); - $attributes = $reflection->getAttributes(\Symfony\Component\Console\Attribute\AsCommand::class); - - if (empty($attributes)) { - fwrite(STDERR, "Warning: Command {$commandClass} missing #[AsCommand] attribute\n"); - continue; - } - - // Get attribute instance - /** @var \Symfony\Component\Console\Attribute\AsCommand $commandAttr */ - $commandAttr = $attributes[0]->newInstance(); - - // Create lazy command wrapper that defers instantiation - $lazyCommand = new LazyCommand( - $commandAttr->name, - [], - $commandAttr->description ?? '', - $commandAttr->hidden ?? false, - fn() => $container->get($commandClass) // Only instantiate when executed - ); - - $console->add($lazyCommand); - - } catch (\Throwable $e) { - fwrite(STDERR, "Warning: Failed to register command {$commandClass}: {$e->getMessage()}\n"); - } - } - - // Run the console application - $exitCode = $console->run(); - exit($exitCode); - + $server = new Server(dirname(__DIR__)); + exit($server->runConsole()); } catch (\Throwable $e) { - fwrite(STDERR, "Fatal error: " . $e->getMessage() . "\n"); - if (isset($app) && $app->debug()) { - fwrite(STDERR, $e->getTraceAsString() . "\n"); + fwrite(STDERR, "Fatal error: {$e->getMessage()}\n"); + if (isset($server) && $server->debug()) { + fwrite(STDERR, $e->getTraceAsString()."\n"); } exit(1); } diff --git a/core/lib/Application.php b/core/lib/Application.php deleted file mode 100644 index b280928..0000000 --- a/core/lib/Application.php +++ /dev/null @@ -1,226 +0,0 @@ -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); - } - - /** - * Run the application - handle incoming request and send response - */ - public function run(): 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); - } - } - - /** - * 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; - } -} diff --git a/core/lib/Server.php b/core/lib/Server.php index b302184..5c200ba 100644 --- a/core/lib/Server.php +++ b/core/lib/Server.php @@ -2,91 +2,310 @@ namespace KTXC; -use KTXC\Injection\Container; +use KTXC\Http\Request\Request; +use KTXC\Http\Response\Response; +use KTXC\Module\ModuleAutoloader; +use KTXC\Module\ModuleManager; +use KTXF\Module\ModuleConsoleInterface; +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Application as ConsoleApplication; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\LazyCommand; /** - * Legacy Server class - now a facade to Application - * @deprecated Use Application class directly + * Server class - entry point for the framework + * Handles configuration loading and kernel lifecycle */ class Server { - public const ENVIRONMENT_DEV = 'dev'; - public const ENVIRONMENT_PROD = 'prod'; + private static $composerLoader = null; + private static ?self $instance = null; + + private Kernel $kernel; + private array $config; + private string $rootDir; - /** - * @deprecated Use Application instead - */ - public static function run(): void { - trigger_error('Server::run() is deprecated. Use Application class instead.', E_USER_DEPRECATED); - - $projectRoot = dirname(dirname(__DIR__)); - $app = new Application($projectRoot); - $app->run(); - } - - /** - * @deprecated Use Application::getInstance()->environment() - */ - public static function environment(): string { - return self::app()->environment(); - } - - /** - * @deprecated Use Application::getInstance()->debug() - */ - public static function debug(): bool { - return self::app()->debug(); - } - - /** - * @deprecated Use Application::getInstance()->kernel() - */ - public static function runtimeKernel(): Kernel { - return self::app()->kernel(); - } - - /** - * @deprecated Use Application::getInstance()->container() - */ - public static function runtimeContainer(): Container { - return self::app()->container(); - } - - /** - * @deprecated Use Application::getInstance()->rootDir() - */ - public static function runtimeRootLocation(): string { - return self::app()->rootDir(); - } - - /** - * @deprecated Use Application::getInstance()->moduleDir() - */ - public static function runtimeModuleLocation(): string { - return self::app()->moduleDir(); - } - - /** - * @deprecated Use Application::setComposerLoader() - */ - public static function setComposerLoader($loader): void { - Application::setComposerLoader($loader); - } - - /** - * @deprecated Use Application::getComposerLoader() - */ - public static function getComposerLoader() { - return Application::getComposerLoader(); - } - - private static function app(): Application + public function __construct(string $rootDir, ?string $environment = null, ?bool $debug = null) { - throw new \RuntimeException( - 'Server class is deprecated and no longer functional. ' . - 'Update your code to use Application class with proper dependency injection. ' . - 'See the migration guide for details.' - ); + self::$instance = $this; + + $this->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"); + } + } } diff --git a/core/lib/index.php b/core/lib/index.php index cae7a9d..824edf2 100644 --- a/core/lib/index.php +++ b/core/lib/index.php @@ -1,22 +1,11 @@ moduleDir()); -$moduleAutoloader->register(); - -$app->run(); \ No newline at end of file +$server->runHttp(); \ No newline at end of file