From a576d85d74294dcc11fb789af88e9ddd60e817bd Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 20 Feb 2026 16:22:57 -0500 Subject: [PATCH 1/4] improve loggers Signed-off-by: Sebastian Krupinski --- core/lib/Application.php | 10 +++++ core/lib/Logger/FileLogger.php | 2 +- core/lib/Logger/PlainFileLogger.php | 62 +++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 core/lib/Logger/PlainFileLogger.php diff --git a/core/lib/Application.php b/core/lib/Application.php index ef48cc3..b280928 100644 --- a/core/lib/Application.php +++ b/core/lib/Application.php @@ -105,6 +105,16 @@ class Application return $this->rootDir . '/modules'; } + public function varDir(): string + { + return $this->rootDir . '/var'; + } + + public function logDir(): string + { + return $this->varDir() . '/logs'; + } + /** * Get configuration value */ diff --git a/core/lib/Logger/FileLogger.php b/core/lib/Logger/FileLogger.php index b3d2988..4662079 100644 --- a/core/lib/Logger/FileLogger.php +++ b/core/lib/Logger/FileLogger.php @@ -26,7 +26,7 @@ class FileLogger implements LoggerInterface if (!is_dir($logDir)) { @mkdir($logDir, 0775, true); } - $this->logFile = rtrim($logDir, '/').'/'.$channel.'.log'; + $this->logFile = rtrim($logDir, '/').'/'.$channel.'.jsonl'; } public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } diff --git a/core/lib/Logger/PlainFileLogger.php b/core/lib/Logger/PlainFileLogger.php new file mode 100644 index 0000000..89b5bce --- /dev/null +++ b/core/lib/Logger/PlainFileLogger.php @@ -0,0 +1,62 @@ +logFile = rtrim($logDir, '/') . '/' . $channel . '.log'; + } + + public function emergency($message, array $context = []): void { $this->log('emergency', $message, $context); } + public function alert($message, array $context = []): void { $this->log('alert', $message, $context); } + public function critical($message, array $context = []): void { $this->log('critical', $message, $context); } + public function error($message, array $context = []): void { $this->log('error', $message, $context); } + public function warning($message, array $context = []): void { $this->log('warning', $message, $context); } + public function notice($message, array $context = []): void { $this->log('notice', $message, $context); } + public function info($message, array $context = []): void { $this->log('info', $message, $context); } + public function debug($message, array $context = []): void { $this->log('debug', $message, $context); } + + public function log($level, $message, array $context = []): void + { + $dt = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true))); + $timestamp = $dt?->format('Y-m-d H:i:s.u') ?? date('Y-m-d H:i:s'); + + $line = $timestamp . ' ' . $this->interpolate((string) $message, $context) . PHP_EOL; + + @file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX); + } + + private function interpolate(string $message, array $context): string + { + if (!str_contains($message, '{')) { + return $message; + } + $replace = []; + foreach ($context as $key => $val) { + if (is_array($val) || (is_object($val) && !method_exists($val, '__toString'))) { + continue; + } + $replace['{' . $key . '}'] = (string) $val; + } + return strtr($message, $replace); + } +} From 8e931f6650bc2f3f6f76c6042cde30b18008d7cb Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 20 Feb 2026 16:23:43 -0500 Subject: [PATCH 2/4] feat: add entity streaming Signed-off-by: Sebastian Krupinski --- .../Http/Response/StreamedNdJsonResponse.php | 76 +++++++++++++++++++ .../lib/Mail/Service/ServiceBaseInterface.php | 16 ++++ 2 files changed, 92 insertions(+) create mode 100644 core/lib/Http/Response/StreamedNdJsonResponse.php diff --git a/core/lib/Http/Response/StreamedNdJsonResponse.php b/core/lib/Http/Response/StreamedNdJsonResponse.php new file mode 100644 index 0000000..dd0429a --- /dev/null +++ b/core/lib/Http/Response/StreamedNdJsonResponse.php @@ -0,0 +1,76 @@ + 1, 'name' => 'Alice']; + * yield ['id' => 2, 'name' => 'Bob']; + * } + * + * return new StreamedNdJsonResponse(records()); + */ +class StreamedNdJsonResponse extends StreamedResponse +{ + /** + * @param iterable $items Items to serialize; each becomes one JSON line + * @param int $flushInterval Flush to client after this many items (default 10) + * @param int $status HTTP status code (default 200) + * @param array $headers Additional HTTP headers + * @param int $encodingOptions Flags passed to json_encode() + */ + public function __construct( + iterable $items, + int $flushInterval = 10, + int $status = 200, + array $headers = [], + private readonly int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, + ) { + parent::__construct(null, $status, $headers); + + if (!$this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/x-ndjson'); + } + + if (!$this->headers->has('X-Accel-Buffering')) { + $this->headers->set('X-Accel-Buffering', 'no'); + } + + $encodingOptions = $this->encodingOptions; + + $this->setCallback(static function () use ($items, $flushInterval, $encodingOptions): void { + $count = 0; + + foreach ($items as $item) { + echo json_encode($item, \JSON_THROW_ON_ERROR | $encodingOptions) . "\n"; + $count++; + + if ($count >= $flushInterval) { + @ob_flush(); + flush(); + $count = 0; + } + } + + // final flush for any remaining buffered items + if ($count > 0) { + @ob_flush(); + flush(); + } + }); + } +} diff --git a/shared/lib/Mail/Service/ServiceBaseInterface.php b/shared/lib/Mail/Service/ServiceBaseInterface.php index 4de6e5c..9745c4b 100644 --- a/shared/lib/Mail/Service/ServiceBaseInterface.php +++ b/shared/lib/Mail/Service/ServiceBaseInterface.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace KTXF\Mail\Service; +use Generator; use KTXF\Mail\Collection\CollectionBaseInterface; use KTXF\Mail\Object\AddressInterface; use KTXF\Resource\Delta\Delta; @@ -168,6 +169,21 @@ interface ServiceBaseInterface extends ResourceServiceBaseInterface { */ public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array; + /** + * Lists messages in a collection + * + * @since 2025.05.01 + * + * @param string|int $collection Collection ID + * @param IFilter|null $filter Optional filter criteria + * @param ISort|null $sort Optional sort order + * @param IRange|null $range Optional pagination + * @param array|null $properties Optional message properties to fetch + * + * @return Generator Yields messages one by one as EntityBaseInterface + */ + public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator; + /** * Creates a filter builder for messages * From c310b96a26f7ca432495de4bafbc7a1e1c375c84 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 20 Feb 2026 16:24:08 -0500 Subject: [PATCH 3/4] fix: message flags Signed-off-by: Sebastian Krupinski --- shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php | 3 +++ shared/lib/Mail/Object/MessagePropertiesBaseInterface.php | 1 + 2 files changed, 4 insertions(+) diff --git a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php index 597bd6e..1ae32b0 100644 --- a/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php +++ b/shared/lib/Mail/Object/MessagePropertiesBaseAbstract.php @@ -391,6 +391,9 @@ abstract class MessagePropertiesBaseAbstract extends NodePropertiesBaseAbstract if (!empty($this->data['attachments'])) { $data[self::JSON_PROPERTY_ATTACHMENTS] = $this->data['attachments']; } + if (!empty($this->data['flags'])) { + $data[self::JSON_PROPERTY_FLAGS] = $this->data['flags']; + } $data[self::JSON_PROPERTY_SUBJECT] = $this->data['subject'] ?? null; $data[self::JSON_PROPERTY_BODY] = $this->data['body'] ?? null; diff --git a/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php b/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php index 6daee90..b3c92ef 100644 --- a/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php +++ b/shared/lib/Mail/Object/MessagePropertiesBaseInterface.php @@ -37,6 +37,7 @@ interface MessagePropertiesBaseInterface extends NodePropertiesBaseInterface { public const JSON_PROPERTY_SNIPPET = 'snippet'; public const JSON_PROPERTY_BODY = 'body'; public const JSON_PROPERTY_ATTACHMENTS = 'attachments'; + public const JSON_PROPERTY_FLAGS = 'flags'; public const JSON_PROPERTY_TAGS = 'tags'; /** From d81e894c81e040c8a4a850e9b48a60c5446a9171 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 20 Feb 2026 17:48:31 -0500 Subject: [PATCH 4/4] feat: unify kernel entry Signed-off-by: Sebastian Krupinski --- bin/console | 101 +---------- core/lib/Application.php | 226 ----------------------- core/lib/Server.php | 375 +++++++++++++++++++++++++++++++-------- core/lib/index.php | 21 +-- 4 files changed, 308 insertions(+), 415 deletions(-) delete mode 100644 core/lib/Application.php 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