feat: unify kernel entry
All checks were successful
JS Unit Tests / test (pull_request) Successful in 38s
Build Test / build (pull_request) Successful in 41s
PHP Unit Tests / test (pull_request) Successful in 50s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-02-20 17:48:31 -05:00
parent c310b96a26
commit d81e894c81
4 changed files with 308 additions and 415 deletions

View File

@@ -3,21 +3,12 @@
/** /**
* Console Entry Point * 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); declare(strict_types=1);
use KTXC\Application; use KTXC\Server;
use KTXC\Kernel;
use KTXC\Module\ModuleManager;
use KTXF\Module\ModuleConsoleInterface;
use Symfony\Component\Console\Application as ConsoleApplication;
use Symfony\Component\Console\Command\LazyCommand;
// Check dependencies
if (!is_dir(dirname(__DIR__).'/vendor')) { if (!is_dir(dirname(__DIR__).'/vendor')) {
fwrite(STDERR, "Dependencies are missing. Run 'composer install' first.\n"); fwrite(STDERR, "Dependencies are missing. Run 'composer install' first.\n");
exit(1); exit(1);
@@ -26,92 +17,12 @@ if (!is_dir(dirname(__DIR__).'/vendor')) {
require_once dirname(__DIR__).'/vendor/autoload.php'; require_once dirname(__DIR__).'/vendor/autoload.php';
try { try {
// Bootstrap the application $server = new Server(dirname(__DIR__));
$projectRoot = dirname(__DIR__); exit($server->runConsole());
$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);
} catch (\Throwable $e) { } catch (\Throwable $e) {
fwrite(STDERR, "Fatal error: " . $e->getMessage() . "\n"); fwrite(STDERR, "Fatal error: {$e->getMessage()}\n");
if (isset($app) && $app->debug()) { if (isset($server) && $server->debug()) {
fwrite(STDERR, $e->getTraceAsString() . "\n"); fwrite(STDERR, $e->getTraceAsString()."\n");
} }
exit(1); exit(1);
} }

View File

@@ -1,226 +0,0 @@
<?php
namespace KTXC;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use Psr\Container\ContainerInterface;
/**
* Application class - entry point for the framework
* Handles configuration loading and kernel lifecycle
*/
class Application
{
private static $composerLoader = null;
private Kernel $kernel;
private array $config;
private string $rootDir;
public function __construct(string $rootDir, ?string $environment = null, ?bool $debug = null)
{
$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);
}
/**
* 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()
? '<pre>' . htmlspecialchars((string) $e) . '</pre>'
: '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;
}
}

View File

@@ -2,91 +2,310 @@
namespace KTXC; 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 * Server class - entry point for the framework
* @deprecated Use Application class directly * Handles configuration loading and kernel lifecycle
*/ */
class Server class Server
{ {
public const ENVIRONMENT_DEV = 'dev'; private static $composerLoader = null;
public const ENVIRONMENT_PROD = 'prod'; private static ?self $instance = null;
/** private Kernel $kernel;
* @deprecated Use Application instead private array $config;
*/ private string $rootDir;
public static function run(): void {
trigger_error('Server::run() is deprecated. Use Application class instead.', E_USER_DEPRECATED);
$projectRoot = dirname(dirname(__DIR__)); public function __construct(string $rootDir, ?string $environment = null, ?bool $debug = null)
$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
{ {
throw new \RuntimeException( self::$instance = $this;
'Server class is deprecated and no longer functional. ' .
'Update your code to use Application class with proper dependency injection. ' . $this->rootDir = $this->resolveProjectRoot($rootDir);
'See the migration guide for details.'
); // 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()
? '<pre>' . htmlspecialchars((string) $e) . '</pre>'
: '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");
}
}
} }

View File

@@ -1,22 +1,11 @@
<?php <?php
use KTXC\Application; use KTXC\Server;
use KTXC\Module\ModuleAutoloader;
// Capture Composer ClassLoader instance // Capture Composer ClassLoader instance for compatibility
$composerLoader = require_once __DIR__ . '/../vendor/autoload.php'; $composerLoader = require_once __DIR__ . '/../vendor/autoload.php';
// Determine project root (one level up from this file) $server = new Server(dirname(__DIR__));
$projectRoot = dirname(__DIR__); Server::setComposerLoader($composerLoader);
// Create and run application $server->runHttp();
$app = new Application($projectRoot);
// Store composer loader for compatibility
Application::setComposerLoader($composerLoader);
// Register custom module autoloader for lazy loading
$moduleAutoloader = new ModuleAutoloader($app->moduleDir());
$moduleAutoloader->register();
$app->run();