From 658a319ded1f2ed91b997bef7bba9530379aaf41 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 21 Dec 2025 19:33:47 -0500 Subject: [PATCH] kernal clean-up --- config/system.php | 17 +- core/lib/Application.php | 216 ++++++++++++++++ core/lib/Controllers/DefaultController.php | 15 +- core/lib/Db/DataStore.php | 2 +- .../Middleware/AuthenticationMiddleware.php | 38 +++ .../Http/Middleware/FirewallMiddleware.php | 32 +++ .../Http/Middleware/MiddlewareInterface.php | 21 ++ .../Http/Middleware/MiddlewarePipeline.php | 112 +++++++++ .../Middleware/RequestHandlerInterface.php | 20 ++ core/lib/Http/Middleware/RouterMiddleware.php | 50 ++++ core/lib/Http/Middleware/TenantMiddleware.php | 35 +++ core/lib/Kernel.php | 193 ++++++++++----- core/lib/Module/ModuleAutoloader.php | 4 +- core/lib/Module/ModuleManager.php | 14 +- core/lib/Resource/ProviderManager.php | 5 +- core/lib/Routing/Router.php | 121 +++++---- core/lib/Server.php | 232 +++++------------- core/lib/Service/UserManagerService.php | 8 +- core/lib/SessionTenant.php | 7 +- core/lib/index.php | 18 +- core/src/layouts/header/SearchBarPanel.vue | 2 +- core/src/utils/modules.ts | 4 + 22 files changed, 832 insertions(+), 334 deletions(-) create mode 100644 core/lib/Application.php create mode 100644 core/lib/Http/Middleware/AuthenticationMiddleware.php create mode 100644 core/lib/Http/Middleware/FirewallMiddleware.php create mode 100644 core/lib/Http/Middleware/MiddlewareInterface.php create mode 100644 core/lib/Http/Middleware/MiddlewarePipeline.php create mode 100644 core/lib/Http/Middleware/RequestHandlerInterface.php create mode 100644 core/lib/Http/Middleware/RouterMiddleware.php create mode 100644 core/lib/Http/Middleware/TenantMiddleware.php diff --git a/config/system.php b/config/system.php index 18b3160..fb5bc4b 100644 --- a/config/system.php +++ b/config/system.php @@ -1,6 +1,10 @@ 'Ktrix', + 'environment' => 'dev', + 'debug' => true, // Database Configuration 'database' => [ // MongoDB connection URI (include credentials if needed) @@ -33,17 +37,4 @@ return [ // Security Configuration 'security.salt' => 'a5418ed8c120b9d12c793ccea10571b74d0dcd4a4db7ca2f75e80fbdafb2bd9b', - - // Application Configuration - 'app' => [ - 'environment' => 'dev', - 'debug' => true, - 'name' => 'Ktrix', - ], - - // Domain Configuration - //'domain' => [ - // 'default' => 'ktrix', - //], - ]; diff --git a/core/lib/Application.php b/core/lib/Application.php new file mode 100644 index 0000000..ef48cc3 --- /dev/null +++ b/core/lib/Application.php @@ -0,0 +1,216 @@ +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'; + } + + /** + * 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/Controllers/DefaultController.php b/core/lib/Controllers/DefaultController.php index 248a3d4..63ae379 100644 --- a/core/lib/Controllers/DefaultController.php +++ b/core/lib/Controllers/DefaultController.php @@ -2,24 +2,23 @@ namespace KTXC\Controllers; +use DI\Attribute\Inject; use KTXC\Http\Response\Response; use KTXC\Http\Response\FileResponse; use KTXC\Http\Response\JsonResponse; use KTXC\Http\Response\RedirectResponse; -use KTXC\Server; use KTXF\Controller\ControllerAbstract; use KTXF\Routing\Attributes\AnonymousRoute; use KTXC\Service\SecurityService; use KTXC\SessionIdentity; -use KTXC\SessionTenant; use KTXC\Http\Request\Request; class DefaultController extends ControllerAbstract { public function __construct( private readonly SecurityService $securityService, - private readonly SessionTenant $tenant, private readonly SessionIdentity $identity, + #[Inject('rootDir')] private readonly string $rootDir, ) {} #[AnonymousRoute('/', name: 'root', methods: ['GET'])] @@ -28,7 +27,7 @@ class DefaultController extends ControllerAbstract // If an authenticated identity is available, serve the private app if ($this->identity->identifier()) { return new FileResponse( - Server::runtimeRootLocation() . '/public/private.html', + $this->rootDir . '/public/private.html', Response::HTTP_OK, ['Content-Type' => 'text/html'] ); @@ -37,7 +36,7 @@ class DefaultController extends ControllerAbstract // User is not authenticated - serve the public app // If there's an accessToken cookie present but invalid, clear it $response = new FileResponse( - Server::runtimeRootLocation() . '/public/public.html', + $this->rootDir . '/public/public.html', Response::HTTP_OK, ['Content-Type' => 'text/html'] ); @@ -57,7 +56,7 @@ class DefaultController extends ControllerAbstract public function login(): Response { return new FileResponse( - Server::runtimeRootLocation() . '/public/public.html', + $this->rootDir . '/public/public.html', Response::HTTP_OK, ['Content-Type' => 'text/html'] ); @@ -119,7 +118,7 @@ class DefaultController extends ControllerAbstract // If an authenticated identity is available, serve the private app if ($this->identity->identifier()) { return new FileResponse( - Server::runtimeRootLocation() . '/public/private.html', + $this->rootDir . '/public/private.html', Response::HTTP_OK, ['Content-Type' => 'text/html'] ); @@ -127,7 +126,7 @@ class DefaultController extends ControllerAbstract // User is not authenticated - serve the public app $response = new FileResponse( - Server::runtimeRootLocation() . '/public/public.html', + $this->rootDir . '/public/public.html', Response::HTTP_OK, ['Content-Type' => 'text/html'] ); diff --git a/core/lib/Db/DataStore.php b/core/lib/Db/DataStore.php index 2bbbccf..91b604a 100644 --- a/core/lib/Db/DataStore.php +++ b/core/lib/Db/DataStore.php @@ -14,7 +14,7 @@ class DataStore protected Client $client; protected Database $database; - public function __construct(#[Inject('database')] array $configuration) + public function __construct(#[Inject('database')] array $configuration = []) { $this->configuration = $configuration; diff --git a/core/lib/Http/Middleware/AuthenticationMiddleware.php b/core/lib/Http/Middleware/AuthenticationMiddleware.php new file mode 100644 index 0000000..73a72ab --- /dev/null +++ b/core/lib/Http/Middleware/AuthenticationMiddleware.php @@ -0,0 +1,38 @@ +securityService->authenticate($request); + + // Initialize session identity if authentication succeeded + if ($identity) { + $this->sessionIdentity->initialize($identity, true); + } + + // Continue to next middleware (authentication is optional at this stage) + return $handler->handle($request); + } +} diff --git a/core/lib/Http/Middleware/FirewallMiddleware.php b/core/lib/Http/Middleware/FirewallMiddleware.php new file mode 100644 index 0000000..ebaf437 --- /dev/null +++ b/core/lib/Http/Middleware/FirewallMiddleware.php @@ -0,0 +1,32 @@ +firewall->authorized($request)) { + return new Response( + Response::$statusTexts[Response::HTTP_FORBIDDEN], + Response::HTTP_FORBIDDEN + ); + } + + // Continue to next middleware + return $handler->handle($request); + } +} diff --git a/core/lib/Http/Middleware/MiddlewareInterface.php b/core/lib/Http/Middleware/MiddlewareInterface.php new file mode 100644 index 0000000..8806316 --- /dev/null +++ b/core/lib/Http/Middleware/MiddlewareInterface.php @@ -0,0 +1,21 @@ + */ + private array $middleware = []; + + private ?ContainerInterface $container = null; + + public function __construct(?ContainerInterface $container = null) + { + $this->container = $container; + } + + /** + * Add middleware to the pipeline + * + * @param string|MiddlewareInterface $middleware Middleware class name or instance + * @return self + */ + public function pipe(string|MiddlewareInterface $middleware): self + { + $this->middleware[] = $middleware; + return $this; + } + + /** + * Handle the request through the middleware pipeline + * + * @param Request $request + * @return Response + */ + public function handle(Request $request): Response + { + // Create a handler for the pipeline + $handler = $this->createHandler(0); + + return $handler->handle($request); + } + + /** + * Create a handler for a specific position in the pipeline + * + * @param int $index Current position in the middleware stack + * @return RequestHandlerInterface + */ + public function createHandler(int $index): RequestHandlerInterface + { + // If we've reached the end of the pipeline, return a default handler + if (!isset($this->middleware[$index])) { + return new class implements RequestHandlerInterface { + public function handle(Request $request): Response { + return new Response(Response::$statusTexts[Response::HTTP_NOT_FOUND], Response::HTTP_NOT_FOUND); + } + }; + } + + return new class($this->middleware[$index], $this, $index, $this->container) implements RequestHandlerInterface { + private string|MiddlewareInterface $middleware; + private MiddlewarePipeline $pipeline; + private int $index; + private ?ContainerInterface $container; + + public function __construct( + string|MiddlewareInterface $middleware, + MiddlewarePipeline $pipeline, + int $index, + ?ContainerInterface $container + ) { + $this->middleware = $middleware; + $this->pipeline = $pipeline; + $this->index = $index; + $this->container = $container; + } + + public function handle(Request $request): Response + { + // Resolve middleware instance if it's a class name + $middleware = $this->middleware; + + if (is_string($middleware)) { + if ($this->container && $this->container->has($middleware)) { + $middleware = $this->container->get($middleware); + } else { + $middleware = new $middleware(); + } + } + + if (!$middleware instanceof MiddlewareInterface) { + throw new \RuntimeException( + sprintf('Middleware must implement %s', MiddlewareInterface::class) + ); + } + + // Create the next handler in the chain + $next = $this->pipeline->createHandler($this->index + 1); + + // Process this middleware + return $middleware->process($request, $next); + } + }; + } +} diff --git a/core/lib/Http/Middleware/RequestHandlerInterface.php b/core/lib/Http/Middleware/RequestHandlerInterface.php new file mode 100644 index 0000000..4177755 --- /dev/null +++ b/core/lib/Http/Middleware/RequestHandlerInterface.php @@ -0,0 +1,20 @@ +router->match($request); + + if (!$match instanceof Route) { + // No route matched, continue to next handler (will return 404) + return $handler->handle($request); + } + + // Check if route requires authentication + if ($match->authenticated && $this->sessionIdentity->identity() === null) { + return new Response( + Response::$statusTexts[Response::HTTP_UNAUTHORIZED], + Response::HTTP_UNAUTHORIZED + ); + } + + // Dispatch to the controller + $response = $this->router->dispatch($match, $request); + + if ($response instanceof Response) { + return $response; + } + + // If dispatch didn't return a response, continue to next handler + return $handler->handle($request); + } +} diff --git a/core/lib/Http/Middleware/TenantMiddleware.php b/core/lib/Http/Middleware/TenantMiddleware.php new file mode 100644 index 0000000..8f9be5d --- /dev/null +++ b/core/lib/Http/Middleware/TenantMiddleware.php @@ -0,0 +1,35 @@ +sessionTenant->configure($request->getHost()); + + // Check if tenant is configured and enabled + if (!$this->sessionTenant->configured() || !$this->sessionTenant->enabled()) { + return new Response( + Response::$statusTexts[Response::HTTP_UNAUTHORIZED], + Response::HTTP_UNAUTHORIZED + ); + } + + // Continue to next middleware + return $handler->handle($request); + } +} diff --git a/core/lib/Kernel.php b/core/lib/Kernel.php index 02259e6..c63acbe 100644 --- a/core/lib/Kernel.php +++ b/core/lib/Kernel.php @@ -11,16 +11,17 @@ namespace KTXC; use KTXC\Http\Request\Request; use KTXC\Http\Response\Response; +use KTXC\Http\Middleware\MiddlewarePipeline; +use KTXC\Http\Middleware\TenantMiddleware; +use KTXC\Http\Middleware\FirewallMiddleware; +use KTXC\Http\Middleware\AuthenticationMiddleware; +use KTXC\Http\Middleware\RouterMiddleware; use KTXC\Injection\Builder; use KTXC\Injection\Container; use Psr\Container\ContainerInterface; use KTXC\Module\ModuleManager; use Psr\Log\LoggerInterface; use KTXC\Logger\FileLogger; -use KTXC\Routing\Router; -use KTXC\Routing\Route; -use KTXC\Service\SecurityService; -use KTXC\Service\FirewallService; use KTXF\Event\EventBus; use KTXF\Cache\EphemeralCacheInterface; use KTXF\Cache\PersistentCacheInterface; @@ -42,16 +43,27 @@ class Kernel protected bool $booted = false; protected ?float $startTime = null; protected ?ContainerInterface $container = null; + protected ?LoggerInterface $logger = null; + protected ?MiddlewarePipeline $pipeline = null; private string $projectDir; + private array $config; public function __construct( protected string $environment = 'prod', protected bool $debug = false, + array $config = [], + ?string $projectDir = null, ) { if (!$environment) { throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); } + + $this->config = $config; + + if ($projectDir !== null) { + $this->projectDir = $projectDir; + } } public function __clone() @@ -63,6 +75,7 @@ class Kernel private function initialize(): void { + if ($this->debug) { $this->startTime = microtime(true); } @@ -74,12 +87,90 @@ class Kernel $_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) { @@ -90,6 +181,10 @@ class Kernel /** @var ModuleManager $moduleManager */ $moduleManager = $this->container->get(ModuleManager::class); $moduleManager->modulesBoot(); + + // Build middleware pipeline + $this->pipeline = $this->buildMiddlewarePipeline(); + $this->booted = true; } } @@ -117,46 +212,24 @@ class Kernel $this->boot(); } - /** @var SessionTenant $sessionTenant */ - $sessionTenant = $this->container->get(SessionTenant::class); - $sessionTenant->configure($request->getHost()); - if (!$sessionTenant->configured() && !$sessionTenant->enabled()) { - return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED); - } + // Use middleware pipeline to handle the request + return $this->pipeline->handle($request); + } - /** @var FirewallService $firewall */ - $firewall = $this->container->get(FirewallService::class); - if (!$firewall->authorized($request)) { - return new Response(Response::$statusTexts[Response::HTTP_FORBIDDEN], Response::HTTP_FORBIDDEN); - } - - /** @var Router $router */ - $router = $this->container->get(Router::class); - if ($router) { - $match = $router->match($request); - if ($match instanceof Route) { - /** @var SecurityService $securityService */ - $securityService = $this->container->get(SecurityService::class); - $identity = $securityService->authenticate($request); - - if ($match->authenticated && $identity === null) { - return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED); - } - - if ($identity) { - /** @var SessionIdentity $sessionIdentity */ - $sessionIdentity = $this->container->get(SessionIdentity::class); - $sessionIdentity->initialize($identity, true); - } - - $response = $router->dispatch($match, $request); - if ($response instanceof Response) { - return $response; - } - } - } - - return new Response(Response::$statusTexts[Response::HTTP_NOT_FOUND], Response::HTTP_NOT_FOUND); + /** + * 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; } /** @@ -263,7 +336,6 @@ class Kernel public function getBuildDir(): string { - // Returns $this->getCacheDir() for backward compatibility return $this->getCacheDir(); } @@ -277,14 +349,6 @@ class Kernel return 'UTF-8'; } - /** - * Gets a new container builder instance used to build the service container. - */ - protected function containerBuilder(): Builder - { - return new Builder(Container::class); - } - /** * Initializes the service container */ @@ -303,10 +367,12 @@ class Kernel */ protected function buildContainer(): Container { - $builder = $this->containerBuilder(); + $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(); @@ -314,14 +380,25 @@ class Kernel protected function configureContainer(Builder $builder): void { - $builder->addDefinitions($this->getConfigDir() . '/system.php'); // Service definitions - $logDir = $this->getLogDir(); $projectDir = $this->folderRoot(); + $moduleDir = $projectDir . '/modules'; + $environment = $this->environment; + $builder->addDefinitions([ - LoggerInterface::class => function() use ($logDir) { - return new FileLogger($logDir); - }, + + // 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) diff --git a/core/lib/Module/ModuleAutoloader.php b/core/lib/Module/ModuleAutoloader.php index ea840fc..e2d5816 100644 --- a/core/lib/Module/ModuleAutoloader.php +++ b/core/lib/Module/ModuleAutoloader.php @@ -2,7 +2,7 @@ namespace KTXC\Module; -use KTXC\Server; +use KTXC\Application; /** * Custom autoloader for modules that allows PascalCase namespaces @@ -73,7 +73,7 @@ class ModuleAutoloader } // Register module namespaces with Composer ClassLoader - $composerLoader = Server::getComposerLoader(); + $composerLoader = \KTXC\Application::getComposerLoader(); if ($composerLoader !== null) { foreach ($this->namespaceMap as $namespace => $folderName) { $composerLoader->addPsr4( diff --git a/core/lib/Module/ModuleManager.php b/core/lib/Module/ModuleManager.php index 0b8a979..c1d0b62 100644 --- a/core/lib/Module/ModuleManager.php +++ b/core/lib/Module/ModuleManager.php @@ -3,9 +3,10 @@ namespace KTXC\Module; use Exception; +use DI\Attribute\Inject; use KTXC\Module\Store\ModuleStore; use KTXC\Module\Store\ModuleEntry; -use KTXC\Server; +use Psr\Container\ContainerInterface; use KTXF\Module\ModuleInstanceInterface; use Psr\Log\LoggerInterface; use ReflectionClass; @@ -17,10 +18,12 @@ class ModuleManager public function __construct( private readonly ModuleStore $repository, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly ContainerInterface $container, + #[Inject('rootDir')] private readonly string $rootDir ) { // Initialize server root path - $this->serverRoot = Server::runtimeRootLocation(); + $this->serverRoot = $rootDir; } /** @@ -393,7 +396,6 @@ class ModuleManager } // For modules with dependencies, try to resolve them from the container - $container = Server::runtimeContainer(); $parameters = $constructor->getParameters(); $args = []; @@ -403,8 +405,8 @@ class ModuleManager $typeName = $type->getName(); // Try to get service from container - if ($container->has($typeName)) { - $args[] = $container->get($typeName); + if ($this->container->has($typeName)) { + $args[] = $this->container->get($typeName); } elseif ($parameter->isDefaultValueAvailable()) { $args[] = $parameter->getDefaultValue(); } else { diff --git a/core/lib/Resource/ProviderManager.php b/core/lib/Resource/ProviderManager.php index f4847aa..ed4e08b 100644 --- a/core/lib/Resource/ProviderManager.php +++ b/core/lib/Resource/ProviderManager.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace KTXC\Resource; -use KTXC\Server; +use Psr\Container\ContainerInterface; use KTXF\Resource\Provider\ProviderInterface; /** @@ -18,6 +18,7 @@ class ProviderManager private array $resolvedProviders = []; public function __construct( + private readonly ContainerInterface $container ) {} /** @@ -55,7 +56,7 @@ class ProviderManager } try { - $provider = Server::runtimeContainer()->get($this->registeredProviders[$type][$identifier]); + $provider = $this->container->get($this->registeredProviders[$type][$identifier]); $this->resolvedProviders[$type][$identifier] = $provider; return $provider; } catch (\Exception $e) { diff --git a/core/lib/Routing/Router.php b/core/lib/Routing/Router.php index be0e7f9..0948b90 100644 --- a/core/lib/Routing/Router.php +++ b/core/lib/Routing/Router.php @@ -2,6 +2,7 @@ namespace KTXC\Routing; +use DI\Attribute\Inject; use KTXC\Http\Request\Request; use KTXC\Http\Response\Response; use KTXC\Injection\Container; @@ -11,7 +12,6 @@ use KTXF\Routing\Attributes\AuthenticatedRoute; use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionMethod; -use KTXC\Server; use KTXF\Json\JsonDeserializable; class Router @@ -24,17 +24,21 @@ class Router public function __construct( private readonly LoggerInterface $logger, - private readonly ModuleManager $moduleManager + private readonly ModuleManager $moduleManager, + Container $container, + #[Inject('rootDir')] private readonly string $rootDir, + #[Inject('moduleDir')] private readonly string $moduleDir, + #[Inject('environment')] private readonly string $environment ) { - $this->container = Server::runtimeContainer(); - $this->cacheFile = Server::runtimeRootLocation() . '/var/cache/routes.cache.php'; + $this->container = $container; + $this->cacheFile = $rootDir . '/var/cache/routes.cache.php'; } private function initialize(): void { // load cached routes in production - if (Server::environment() === Server::ENVIRONMENT_PROD && file_exists($this->cacheFile)) { + if ($this->environment === 'prod' && file_exists($this->cacheFile)) { $data = include $this->cacheFile; if (is_array($data)) { $this->routes = $data; @@ -55,13 +59,13 @@ class Router private function scan(): void { // load core controllers - foreach (glob(Server::runtimeRootLocation() . '/core/lib/Controllers/*.php') as $file) { + foreach (glob($this->rootDir . '/core/lib/Controllers/*.php') as $file) { $this->extract($file); } // load module controllers foreach ($this->moduleManager->list(true, true) as $module) { - $path = Server::runtimeModuleLocation() . '/' . $module->handle() . '/lib/Controllers'; + $path = $this->moduleDir . '/' . $module->handle() . '/lib/Controllers'; if (is_dir($path)) { foreach (glob($path . '/*.php') as $file) { $this->extract($file, '/m/' . $module->handle()); @@ -182,67 +186,58 @@ class Router $routeControllerName = $route->className; $routeControllerMethod = $route->classMethodName; $routeControllerParameters = $route->classMethodParameters; - //try { - // instantiate controller - if ($this->container->has($routeControllerName)) { - $instance = $this->container->get($routeControllerName); - } else { - $instance = new $routeControllerName(); + // instantiate controller + if ($this->container->has($routeControllerName)) { + $instance = $this->container->get($routeControllerName); + } else { + $instance = new $routeControllerName(); + } + try { + $requestParameters = $request->getPayload(); + } catch (\Throwable) { + // ignore payload errors + } + $reflectionMethod = new \ReflectionMethod($routeControllerName, $routeControllerMethod); + $routeParams = $route->params ?? []; + $callArgs = []; + foreach ($reflectionMethod->getParameters() as $reflectionParameter) { + $reflectionParameterName = $reflectionParameter->getName(); + $reflectionParameterType = $reflectionParameter->getType(); + // if parameter matches request class, use current request + if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), Request::class, true)) { + $callArgs[] = $request; + continue; } - try { - $requestParameters = $request->getPayload(); - } catch (\Throwable) { - // ignore payload errors + // if method parameter matches a route path param, use that (highest priority) + if (array_key_exists($reflectionParameterName, $routeParams)) { + $callArgs[] = $routeParams[$reflectionParameterName]; + continue; } - $reflectionMethod = new \ReflectionMethod($routeControllerName, $routeControllerMethod); - $routeParams = $route->params ?? []; - $callArgs = []; - foreach ($reflectionMethod->getParameters() as $reflectionParameter) { - $reflectionParameterName = $reflectionParameter->getName(); - $reflectionParameterType = $reflectionParameter->getType(); - // if parameter matches request class, use current request - if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), Request::class, true)) { - $callArgs[] = $request; - continue; - } - // if method parameter matches a route path param, use that (highest priority) - if (array_key_exists($reflectionParameterName, $routeParams)) { - $callArgs[] = $routeParams[$reflectionParameterName]; - continue; - } - // if method parameter matches a request param, use that - if ($requestParameters->has($reflectionParameterName)) { - // if parameter is a class implementing JsonDeserializable, call jsonDeserialize on it - if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), JsonDeserializable::class, true)) { - $type = $reflectionParameterType->getName(); - $object = new $type(); - if ($object instanceof JsonDeserializable) { - $object->jsonDeserialize($requestParameters->get($reflectionParameterName)); - $callArgs[] = $object; - continue; - } + // if method parameter matches a request param, use that + if ($requestParameters->has($reflectionParameterName)) { + // if parameter is a class implementing JsonDeserializable, call jsonDeserialize on it + if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), JsonDeserializable::class, true)) { + $type = $reflectionParameterType->getName(); + $object = new $type(); + if ($object instanceof JsonDeserializable) { + $object->jsonDeserialize($requestParameters->get($reflectionParameterName)); + $callArgs[] = $object; + continue; } - // otherwise, use the raw value - $callArgs[] = $requestParameters->get($reflectionParameterName); - continue; } - // if method parameter did not match, but has a default value, use that - if ($reflectionParameter->isDefaultValueAvailable()) { - $callArgs[] = $reflectionParameter->getDefaultValue(); - continue; - } - $callArgs[] = null; + // otherwise, use the raw value + $callArgs[] = $requestParameters->get($reflectionParameterName); + continue; } - $result = $instance->$routeControllerMethod(...$callArgs); - return $result instanceof Response ? $result : null; - //} catch (\Throwable $e) { - // $this->logger->error('Route dispatch failed', [ - // 'controller' => $routeControllerName, - // 'method' => $routeControllerMethod, - // 'error' => $e->getMessage(), - // ]); - // throw $e; - //} + // if method parameter did not match, but has a default value, use that + if ($reflectionParameter->isDefaultValueAvailable()) { + $callArgs[] = $reflectionParameter->getDefaultValue(); + continue; + } + $callArgs[] = null; + } + $result = $instance->$routeControllerMethod(...$callArgs); + return $result instanceof Response ? $result : null; } } diff --git a/core/lib/Server.php b/core/lib/Server.php index 9d723e3..b302184 100644 --- a/core/lib/Server.php +++ b/core/lib/Server.php @@ -2,197 +2,91 @@ namespace KTXC; -use ErrorException; -use KTXC\Http\Request\Request; -use KTXC\Http\Response\Response; use KTXC\Injection\Container; -use Psr\Log\LoggerInterface; -use Throwable; /** - * Provides static access to the server container + * Legacy Server class - now a facade to Application + * @deprecated Use Application class directly */ class Server { public const ENVIRONMENT_DEV = 'dev'; public const ENVIRONMENT_PROD = 'prod'; - protected static $kernel; - protected static $composerLoader; - - public static function run() { - // Set up global error handler before anything else - self::setupErrorHandlers(); + /** + * @deprecated Use Application instead + */ + public static function run(): void { + trigger_error('Server::run() is deprecated. Use Application class instead.', E_USER_DEPRECATED); - try { - self::$kernel = new Kernel(self::ENVIRONMENT_DEV, true); - $request = Request::createFromGlobals(); - $response = self::$kernel->handle($request); - if ($response instanceof Response) { - $response->send(); - } - } catch (\Throwable $e) { - self::logException($e); - $content = self::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); - } - } - - public static function environment(): string { - return self::$kernel->environment(); - } - - public static function debug(): bool { - return self::$kernel->debug(); - } - - public static function runtimeKernel(): Kernel { - return self::$kernel; - } - - public static function runtimeContainer(): Container { - return self::$kernel->container(); - } - - public static function runtimeRootLocation(): string { - return self::$kernel->folderRoot(); - } - - public static function runtimeModuleLocation(): string { - return self::$kernel->folderRoot() . '/modules'; + $projectRoot = dirname(dirname(__DIR__)); + $app = new Application($projectRoot); + $app->run(); } /** - * Set the Composer ClassLoader instance + * @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 { - self::$composerLoader = $loader; + Application::setComposerLoader($loader); } /** - * Get the Composer ClassLoader instance + * @deprecated Use Application::getComposerLoader() */ public static function getComposerLoader() { - return self::$composerLoader; + return Application::getComposerLoader(); } - /** - * Set up global error and exception handlers - */ - protected static function setupErrorHandlers(): 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 - ); - - self::logError($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) { - self::logException($exception); - - if (self::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'] - ); - - self::logError($message, $error); - - if (self::debug()) { - echo '
' . $message . '
'; - } else { - echo 'A fatal error occurred. Please try again later.'; - } - } - }); - } - - /** - * Log an error message - */ - protected static function logError(string $message, array $context = []): void { - try { - if (self::$kernel && self::$kernel->container()->has(LoggerInterface::class)) { - $logger = self::$kernel->container()->get(LoggerInterface::class); - $logger->error($message, $context); - } else { - // Fallback to error_log if logger not available - error_log($message . ' ' . json_encode($context)); - } - } catch (Throwable $e) { - // Last resort fallback - error_log('Error logging failed: ' . $e->getMessage()); - error_log($message . ' ' . json_encode($context)); - } - } - - /** - * Log an exception - */ - protected static function logException(Throwable $exception): void { - try { - if (self::$kernel && self::$kernel->container()->has(LoggerInterface::class)) { - $logger = self::$kernel->container()->get(LoggerInterface::class); - $logger->error('Exception caught: ' . $exception->getMessage(), [ - 'exception' => $exception, - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - 'trace' => $exception->getTraceAsString(), - ]); - } else { - // Fallback to error_log if logger not available - error_log('Exception: ' . $exception->getMessage() . ' in ' . $exception->getFile() . ':' . $exception->getLine()); - error_log($exception->getTraceAsString()); - } - } catch (Throwable $e) { - // Last resort fallback - error_log('Exception logging failed: ' . $e->getMessage()); - error_log('Original exception: ' . $exception->getMessage()); - } + private static function app(): Application + { + 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.' + ); } } diff --git a/core/lib/Service/UserManagerService.php b/core/lib/Service/UserManagerService.php index 3ee063c..d8dfb01 100644 --- a/core/lib/Service/UserManagerService.php +++ b/core/lib/Service/UserManagerService.php @@ -4,7 +4,7 @@ namespace KTXC\Service; use KTXC\Identity\Provider\DefaultIdentityProvider; use KTXC\Models\Identity\User; -use KTXC\Server; +use Psr\Container\ContainerInterface; use KTXC\SessionTenant; /** @@ -18,7 +18,8 @@ class UserManagerService public function __construct( private readonly SessionTenant $tenant, - private readonly UserService $userService + private readonly UserService $userService, + private readonly ContainerInterface $container ) { // Register the default identity provider $this->providerRegister('default', DefaultIdentityProvider::class); @@ -51,8 +52,7 @@ class UserManagerService $providerClass = $this->availableIdentityProviders[$identifier]; try { - // Server::get automatically detects context from calling object - $providerInstance = Server::runtimeContainer()->get($providerClass); + $providerInstance = $this->container->get($providerClass); // Cache the instance $this->cachedIdentityProviders[$identifier] = $providerInstance; diff --git a/core/lib/SessionTenant.php b/core/lib/SessionTenant.php index 8de4d66..7400c0e 100644 --- a/core/lib/SessionTenant.php +++ b/core/lib/SessionTenant.php @@ -12,6 +12,10 @@ class SessionTenant private ?string $domain = null; private bool $configured = false; + public function __construct( + private readonly TenantService $tenantService + ) {} + /** * Configure the tenant information * This method is called by the SecurityMiddleware after validation @@ -21,8 +25,7 @@ class SessionTenant if ($this->configured) { return; } - $service = Server::runtimeContainer()->get(TenantService::class); - $tenant = $service->fetchByDomain($domain); + $tenant = $this->tenantService->fetchByDomain($domain); if ($tenant) { $this->domain = $domain; $this->tenant = $tenant; diff --git a/core/lib/index.php b/core/lib/index.php index 82dd86c..cae7a9d 100644 --- a/core/lib/index.php +++ b/core/lib/index.php @@ -1,14 +1,22 @@ moduleDir()); $moduleAutoloader->register(); -Server::run(); \ No newline at end of file +$app->run(); \ No newline at end of file diff --git a/core/src/layouts/header/SearchBarPanel.vue b/core/src/layouts/header/SearchBarPanel.vue index 6516d7a..c7c9daf 100644 --- a/core/src/layouts/header/SearchBarPanel.vue +++ b/core/src/layouts/header/SearchBarPanel.vue @@ -5,7 +5,7 @@ - + diff --git a/core/src/utils/modules.ts b/core/src/utils/modules.ts index bcb2cf1..5bcaa19 100644 --- a/core/src/utils/modules.ts +++ b/core/src/utils/modules.ts @@ -62,6 +62,10 @@ export async function initializeModules(app: App): Promise { const loadPromises: Promise[] = []; for (const [moduleId, moduleInfo] of Object.entries(availableModules)) { + if (!moduleInfo) { + console.warn(`Module ${moduleId} has no configuration, skipping`); + continue; + } if (moduleInfo.handle && moduleInfo.boot && !moduleInfo.booted) { const moduleHandle = moduleInfo.handle; const moduleUrl = `/modules/${moduleInfo.handle}/${moduleInfo.boot}`;