kernal clean-up

This commit is contained in:
root
2025-12-21 19:33:47 -05:00
parent 3ffabfe3a3
commit 658a319ded
22 changed files with 832 additions and 334 deletions

View File

@@ -1,6 +1,10 @@
<?php <?php
return [ return [
// Application Configuration
'name' => 'Ktrix',
'environment' => 'dev',
'debug' => true,
// Database Configuration // Database Configuration
'database' => [ 'database' => [
// MongoDB connection URI (include credentials if needed) // MongoDB connection URI (include credentials if needed)
@@ -33,17 +37,4 @@ return [
// Security Configuration // Security Configuration
'security.salt' => 'a5418ed8c120b9d12c793ccea10571b74d0dcd4a4db7ca2f75e80fbdafb2bd9b', 'security.salt' => 'a5418ed8c120b9d12c793ccea10571b74d0dcd4a4db7ca2f75e80fbdafb2bd9b',
// Application Configuration
'app' => [
'environment' => 'dev',
'debug' => true,
'name' => 'Ktrix',
],
// Domain Configuration
//'domain' => [
// 'default' => 'ktrix',
//],
]; ];

216
core/lib/Application.php Normal file
View File

@@ -0,0 +1,216 @@
<?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';
}
/**
* 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,24 +2,23 @@
namespace KTXC\Controllers; namespace KTXC\Controllers;
use DI\Attribute\Inject;
use KTXC\Http\Response\Response; use KTXC\Http\Response\Response;
use KTXC\Http\Response\FileResponse; use KTXC\Http\Response\FileResponse;
use KTXC\Http\Response\JsonResponse; use KTXC\Http\Response\JsonResponse;
use KTXC\Http\Response\RedirectResponse; use KTXC\Http\Response\RedirectResponse;
use KTXC\Server;
use KTXF\Controller\ControllerAbstract; use KTXF\Controller\ControllerAbstract;
use KTXF\Routing\Attributes\AnonymousRoute; use KTXF\Routing\Attributes\AnonymousRoute;
use KTXC\Service\SecurityService; use KTXC\Service\SecurityService;
use KTXC\SessionIdentity; use KTXC\SessionIdentity;
use KTXC\SessionTenant;
use KTXC\Http\Request\Request; use KTXC\Http\Request\Request;
class DefaultController extends ControllerAbstract class DefaultController extends ControllerAbstract
{ {
public function __construct( public function __construct(
private readonly SecurityService $securityService, private readonly SecurityService $securityService,
private readonly SessionTenant $tenant,
private readonly SessionIdentity $identity, private readonly SessionIdentity $identity,
#[Inject('rootDir')] private readonly string $rootDir,
) {} ) {}
#[AnonymousRoute('/', name: 'root', methods: ['GET'])] #[AnonymousRoute('/', name: 'root', methods: ['GET'])]
@@ -28,7 +27,7 @@ class DefaultController extends ControllerAbstract
// If an authenticated identity is available, serve the private app // If an authenticated identity is available, serve the private app
if ($this->identity->identifier()) { if ($this->identity->identifier()) {
return new FileResponse( return new FileResponse(
Server::runtimeRootLocation() . '/public/private.html', $this->rootDir . '/public/private.html',
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );
@@ -37,7 +36,7 @@ class DefaultController extends ControllerAbstract
// User is not authenticated - serve the public app // User is not authenticated - serve the public app
// If there's an accessToken cookie present but invalid, clear it // If there's an accessToken cookie present but invalid, clear it
$response = new FileResponse( $response = new FileResponse(
Server::runtimeRootLocation() . '/public/public.html', $this->rootDir . '/public/public.html',
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );
@@ -57,7 +56,7 @@ class DefaultController extends ControllerAbstract
public function login(): Response public function login(): Response
{ {
return new FileResponse( return new FileResponse(
Server::runtimeRootLocation() . '/public/public.html', $this->rootDir . '/public/public.html',
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );
@@ -119,7 +118,7 @@ class DefaultController extends ControllerAbstract
// If an authenticated identity is available, serve the private app // If an authenticated identity is available, serve the private app
if ($this->identity->identifier()) { if ($this->identity->identifier()) {
return new FileResponse( return new FileResponse(
Server::runtimeRootLocation() . '/public/private.html', $this->rootDir . '/public/private.html',
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );
@@ -127,7 +126,7 @@ class DefaultController extends ControllerAbstract
// User is not authenticated - serve the public app // User is not authenticated - serve the public app
$response = new FileResponse( $response = new FileResponse(
Server::runtimeRootLocation() . '/public/public.html', $this->rootDir . '/public/public.html',
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/html'] ['Content-Type' => 'text/html']
); );

View File

@@ -14,7 +14,7 @@ class DataStore
protected Client $client; protected Client $client;
protected Database $database; protected Database $database;
public function __construct(#[Inject('database')] array $configuration) public function __construct(#[Inject('database')] array $configuration = [])
{ {
$this->configuration = $configuration; $this->configuration = $configuration;

View File

@@ -0,0 +1,38 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\Service\SecurityService;
use KTXC\SessionIdentity;
/**
* Authentication middleware
* Authenticates the request and initializes session identity
*
* Note: This middleware does NOT enforce authentication.
* It only attempts to authenticate if credentials are present.
* Route-level authentication is enforced by RouterMiddleware.
*/
class AuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly SecurityService $securityService,
private readonly SessionIdentity $sessionIdentity
) {}
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// Attempt to authenticate the request
$identity = $this->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);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\Service\FirewallService;
/**
* Firewall middleware
* Checks if the request is authorized to proceed
*/
class FirewallMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly FirewallService $firewall
) {}
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// Check firewall authorization
if (!$this->firewall->authorized($request)) {
return new Response(
Response::$statusTexts[Response::HTTP_FORBIDDEN],
Response::HTTP_FORBIDDEN
);
}
// Continue to next middleware
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
/**
* PSR-15 style middleware interface
*/
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* @param Request $request The request to process
* @param RequestHandlerInterface $handler The next handler in the pipeline
* @return Response The response from processing
*/
public function process(Request $request, RequestHandlerInterface $handler): Response;
}

View File

@@ -0,0 +1,112 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use Psr\Container\ContainerInterface;
/**
* Middleware pipeline - processes request through a stack of middleware
*/
class MiddlewarePipeline implements RequestHandlerInterface
{
/** @var array<string|MiddlewareInterface> */
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);
}
};
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
/**
* PSR-15 style request handler interface
*/
interface RequestHandlerInterface
{
/**
* Handle the request and return a response.
*
* @param Request $request The request to handle
* @return Response The response from handling the request
*/
public function handle(Request $request): Response;
}

View File

@@ -0,0 +1,50 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\Routing\Router;
use KTXC\Routing\Route;
use KTXC\SessionIdentity;
/**
* Router middleware
* Matches routes and dispatches to controllers
*/
class RouterMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly Router $router,
private readonly SessionIdentity $sessionIdentity
) {}
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// Attempt to match the route
$match = $this->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);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace KTXC\Http\Middleware;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\SessionTenant;
/**
* Tenant resolution middleware
* Configures the tenant based on the request host
*/
class TenantMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly SessionTenant $sessionTenant
) {}
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// Configure tenant from request host
$this->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);
}
}

View File

@@ -11,16 +11,17 @@ namespace KTXC;
use KTXC\Http\Request\Request; use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response; 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\Builder;
use KTXC\Injection\Container; use KTXC\Injection\Container;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use KTXC\Module\ModuleManager; use KTXC\Module\ModuleManager;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use KTXC\Logger\FileLogger; 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\Event\EventBus;
use KTXF\Cache\EphemeralCacheInterface; use KTXF\Cache\EphemeralCacheInterface;
use KTXF\Cache\PersistentCacheInterface; use KTXF\Cache\PersistentCacheInterface;
@@ -42,16 +43,27 @@ class Kernel
protected bool $booted = false; protected bool $booted = false;
protected ?float $startTime = null; protected ?float $startTime = null;
protected ?ContainerInterface $container = null; protected ?ContainerInterface $container = null;
protected ?LoggerInterface $logger = null;
protected ?MiddlewarePipeline $pipeline = null;
private string $projectDir; private string $projectDir;
private array $config;
public function __construct( public function __construct(
protected string $environment = 'prod', protected string $environment = 'prod',
protected bool $debug = false, protected bool $debug = false,
array $config = [],
?string $projectDir = null,
) { ) {
if (!$environment) { if (!$environment) {
throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); 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() public function __clone()
@@ -63,6 +75,7 @@ class Kernel
private function initialize(): void private function initialize(): void
{ {
if ($this->debug) { if ($this->debug) {
$this->startTime = microtime(true); $this->startTime = microtime(true);
} }
@@ -74,12 +87,90 @@ class Kernel
$_SERVER['SHELL_VERBOSITY'] = 3; $_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(); $container = $this->initializeContainer();
$this->container = $container; $this->container = $container;
$this->initialized = true; $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 '<pre>Uncaught Exception: ' . $exception . '</pre>';
} 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 '<pre>' . $message . '</pre>';
} else {
echo 'A fatal error occurred. Please try again later.';
}
}
});
}
public function boot(): void public function boot(): void
{ {
if (!$this->initialized) { if (!$this->initialized) {
@@ -90,6 +181,10 @@ class Kernel
/** @var ModuleManager $moduleManager */ /** @var ModuleManager $moduleManager */
$moduleManager = $this->container->get(ModuleManager::class); $moduleManager = $this->container->get(ModuleManager::class);
$moduleManager->modulesBoot(); $moduleManager->modulesBoot();
// Build middleware pipeline
$this->pipeline = $this->buildMiddlewarePipeline();
$this->booted = true; $this->booted = true;
} }
} }
@@ -117,46 +212,24 @@ class Kernel
$this->boot(); $this->boot();
} }
/** @var SessionTenant $sessionTenant */ // Use middleware pipeline to handle the request
$sessionTenant = $this->container->get(SessionTenant::class); return $this->pipeline->handle($request);
$sessionTenant->configure($request->getHost()); }
if (!$sessionTenant->configured() && !$sessionTenant->enabled()) {
return new Response(Response::$statusTexts[Response::HTTP_UNAUTHORIZED], Response::HTTP_UNAUTHORIZED);
}
/** @var FirewallService $firewall */ /**
$firewall = $this->container->get(FirewallService::class); * Build the middleware pipeline
if (!$firewall->authorized($request)) { */
return new Response(Response::$statusTexts[Response::HTTP_FORBIDDEN], Response::HTTP_FORBIDDEN); protected function buildMiddlewarePipeline(): MiddlewarePipeline
} {
$pipeline = new MiddlewarePipeline($this->container);
/** @var Router $router */ // Register middleware in execution order
$router = $this->container->get(Router::class); $pipeline->pipe(TenantMiddleware::class);
if ($router) { $pipeline->pipe(FirewallMiddleware::class);
$match = $router->match($request); $pipeline->pipe(AuthenticationMiddleware::class);
if ($match instanceof Route) { $pipeline->pipe(RouterMiddleware::class);
/** @var SecurityService $securityService */
$securityService = $this->container->get(SecurityService::class);
$identity = $securityService->authenticate($request);
if ($match->authenticated && $identity === null) { return $pipeline;
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);
} }
/** /**
@@ -263,7 +336,6 @@ class Kernel
public function getBuildDir(): string public function getBuildDir(): string
{ {
// Returns $this->getCacheDir() for backward compatibility
return $this->getCacheDir(); return $this->getCacheDir();
} }
@@ -277,14 +349,6 @@ class Kernel
return 'UTF-8'; 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 * Initializes the service container
*/ */
@@ -303,10 +367,12 @@ class Kernel
*/ */
protected function buildContainer(): Container protected function buildContainer(): Container
{ {
$builder = $this->containerBuilder(); $builder = new Builder(Container::class);
$builder->useAutowiring(true); $builder->useAutowiring(true);
$builder->useAttributes(true); $builder->useAttributes(true);
$builder->addDefinitions($this->parameters()); $builder->addDefinitions($this->parameters());
$builder->addDefinitions($this->config);
$this->configureContainer($builder); $this->configureContainer($builder);
return $builder->build(); return $builder->build();
@@ -314,14 +380,25 @@ class Kernel
protected function configureContainer(Builder $builder): void protected function configureContainer(Builder $builder): void
{ {
$builder->addDefinitions($this->getConfigDir() . '/system.php');
// Service definitions // Service definitions
$logDir = $this->getLogDir();
$projectDir = $this->folderRoot(); $projectDir = $this->folderRoot();
$moduleDir = $projectDir . '/modules';
$environment = $this->environment;
$builder->addDefinitions([ $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 as singleton for consistent event handling
EventBus::class => \DI\create(EventBus::class), EventBus::class => \DI\create(EventBus::class),
// Ephemeral Cache - for short-lived data (sessions, rate limits, challenges) // Ephemeral Cache - for short-lived data (sessions, rate limits, challenges)

View File

@@ -2,7 +2,7 @@
namespace KTXC\Module; namespace KTXC\Module;
use KTXC\Server; use KTXC\Application;
/** /**
* Custom autoloader for modules that allows PascalCase namespaces * Custom autoloader for modules that allows PascalCase namespaces
@@ -73,7 +73,7 @@ class ModuleAutoloader
} }
// Register module namespaces with Composer ClassLoader // Register module namespaces with Composer ClassLoader
$composerLoader = Server::getComposerLoader(); $composerLoader = \KTXC\Application::getComposerLoader();
if ($composerLoader !== null) { if ($composerLoader !== null) {
foreach ($this->namespaceMap as $namespace => $folderName) { foreach ($this->namespaceMap as $namespace => $folderName) {
$composerLoader->addPsr4( $composerLoader->addPsr4(

View File

@@ -3,9 +3,10 @@
namespace KTXC\Module; namespace KTXC\Module;
use Exception; use Exception;
use DI\Attribute\Inject;
use KTXC\Module\Store\ModuleStore; use KTXC\Module\Store\ModuleStore;
use KTXC\Module\Store\ModuleEntry; use KTXC\Module\Store\ModuleEntry;
use KTXC\Server; use Psr\Container\ContainerInterface;
use KTXF\Module\ModuleInstanceInterface; use KTXF\Module\ModuleInstanceInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use ReflectionClass; use ReflectionClass;
@@ -17,10 +18,12 @@ class ModuleManager
public function __construct( public function __construct(
private readonly ModuleStore $repository, 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 // 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 // For modules with dependencies, try to resolve them from the container
$container = Server::runtimeContainer();
$parameters = $constructor->getParameters(); $parameters = $constructor->getParameters();
$args = []; $args = [];
@@ -403,8 +405,8 @@ class ModuleManager
$typeName = $type->getName(); $typeName = $type->getName();
// Try to get service from container // Try to get service from container
if ($container->has($typeName)) { if ($this->container->has($typeName)) {
$args[] = $container->get($typeName); $args[] = $this->container->get($typeName);
} elseif ($parameter->isDefaultValueAvailable()) { } elseif ($parameter->isDefaultValueAvailable()) {
$args[] = $parameter->getDefaultValue(); $args[] = $parameter->getDefaultValue();
} else { } else {

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace KTXC\Resource; namespace KTXC\Resource;
use KTXC\Server; use Psr\Container\ContainerInterface;
use KTXF\Resource\Provider\ProviderInterface; use KTXF\Resource\Provider\ProviderInterface;
/** /**
@@ -18,6 +18,7 @@ class ProviderManager
private array $resolvedProviders = []; private array $resolvedProviders = [];
public function __construct( public function __construct(
private readonly ContainerInterface $container
) {} ) {}
/** /**
@@ -55,7 +56,7 @@ class ProviderManager
} }
try { try {
$provider = Server::runtimeContainer()->get($this->registeredProviders[$type][$identifier]); $provider = $this->container->get($this->registeredProviders[$type][$identifier]);
$this->resolvedProviders[$type][$identifier] = $provider; $this->resolvedProviders[$type][$identifier] = $provider;
return $provider; return $provider;
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -2,6 +2,7 @@
namespace KTXC\Routing; namespace KTXC\Routing;
use DI\Attribute\Inject;
use KTXC\Http\Request\Request; use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response; use KTXC\Http\Response\Response;
use KTXC\Injection\Container; use KTXC\Injection\Container;
@@ -11,7 +12,6 @@ use KTXF\Routing\Attributes\AuthenticatedRoute;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use ReflectionClass; use ReflectionClass;
use ReflectionMethod; use ReflectionMethod;
use KTXC\Server;
use KTXF\Json\JsonDeserializable; use KTXF\Json\JsonDeserializable;
class Router class Router
@@ -24,17 +24,21 @@ class Router
public function __construct( public function __construct(
private readonly LoggerInterface $logger, 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->container = $container;
$this->cacheFile = Server::runtimeRootLocation() . '/var/cache/routes.cache.php'; $this->cacheFile = $rootDir . '/var/cache/routes.cache.php';
} }
private function initialize(): void private function initialize(): void
{ {
// load cached routes in production // 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; $data = include $this->cacheFile;
if (is_array($data)) { if (is_array($data)) {
$this->routes = $data; $this->routes = $data;
@@ -55,13 +59,13 @@ class Router
private function scan(): void private function scan(): void
{ {
// load core controllers // 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); $this->extract($file);
} }
// load module controllers // load module controllers
foreach ($this->moduleManager->list(true, true) as $module) { 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)) { if (is_dir($path)) {
foreach (glob($path . '/*.php') as $file) { foreach (glob($path . '/*.php') as $file) {
$this->extract($file, '/m/' . $module->handle()); $this->extract($file, '/m/' . $module->handle());
@@ -182,67 +186,58 @@ class Router
$routeControllerName = $route->className; $routeControllerName = $route->className;
$routeControllerMethod = $route->classMethodName; $routeControllerMethod = $route->classMethodName;
$routeControllerParameters = $route->classMethodParameters; $routeControllerParameters = $route->classMethodParameters;
//try { // instantiate controller
// instantiate controller if ($this->container->has($routeControllerName)) {
if ($this->container->has($routeControllerName)) { $instance = $this->container->get($routeControllerName);
$instance = $this->container->get($routeControllerName); } else {
} else { $instance = new $routeControllerName();
$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 { // if method parameter matches a route path param, use that (highest priority)
$requestParameters = $request->getPayload(); if (array_key_exists($reflectionParameterName, $routeParams)) {
} catch (\Throwable) { $callArgs[] = $routeParams[$reflectionParameterName];
// ignore payload errors continue;
} }
$reflectionMethod = new \ReflectionMethod($routeControllerName, $routeControllerMethod); // if method parameter matches a request param, use that
$routeParams = $route->params ?? []; if ($requestParameters->has($reflectionParameterName)) {
$callArgs = []; // if parameter is a class implementing JsonDeserializable, call jsonDeserialize on it
foreach ($reflectionMethod->getParameters() as $reflectionParameter) { if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), JsonDeserializable::class, true)) {
$reflectionParameterName = $reflectionParameter->getName(); $type = $reflectionParameterType->getName();
$reflectionParameterType = $reflectionParameter->getType(); $object = new $type();
// if parameter matches request class, use current request if ($object instanceof JsonDeserializable) {
if ($reflectionParameterType && $reflectionParameterType instanceof \ReflectionNamedType && !$reflectionParameterType->isBuiltin() && is_a($reflectionParameterType->getName(), Request::class, true)) { $object->jsonDeserialize($requestParameters->get($reflectionParameterName));
$callArgs[] = $request; $callArgs[] = $object;
continue; 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;
}
} }
// otherwise, use the raw value
$callArgs[] = $requestParameters->get($reflectionParameterName);
continue;
} }
// if method parameter did not match, but has a default value, use that // otherwise, use the raw value
if ($reflectionParameter->isDefaultValueAvailable()) { $callArgs[] = $requestParameters->get($reflectionParameterName);
$callArgs[] = $reflectionParameter->getDefaultValue(); continue;
continue;
}
$callArgs[] = null;
} }
$result = $instance->$routeControllerMethod(...$callArgs); // if method parameter did not match, but has a default value, use that
return $result instanceof Response ? $result : null; if ($reflectionParameter->isDefaultValueAvailable()) {
//} catch (\Throwable $e) { $callArgs[] = $reflectionParameter->getDefaultValue();
// $this->logger->error('Route dispatch failed', [ continue;
// 'controller' => $routeControllerName, }
// 'method' => $routeControllerMethod, $callArgs[] = null;
// 'error' => $e->getMessage(), }
// ]); $result = $instance->$routeControllerMethod(...$callArgs);
// throw $e; return $result instanceof Response ? $result : null;
//}
} }
} }

View File

@@ -2,197 +2,91 @@
namespace KTXC; namespace KTXC;
use ErrorException;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\Injection\Container; 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 class Server
{ {
public const ENVIRONMENT_DEV = 'dev'; public const ENVIRONMENT_DEV = 'dev';
public const ENVIRONMENT_PROD = 'prod'; public const ENVIRONMENT_PROD = 'prod';
protected static $kernel; /**
protected static $composerLoader; * @deprecated Use Application instead
*/
public static function run(): void {
trigger_error('Server::run() is deprecated. Use Application class instead.', E_USER_DEPRECATED);
public static function run() { $projectRoot = dirname(dirname(__DIR__));
// Set up global error handler before anything else $app = new Application($projectRoot);
self::setupErrorHandlers(); $app->run();
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()
? '<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);
}
}
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';
} }
/** /**
* 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 { 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() { public static function getComposerLoader() {
return self::$composerLoader; return Application::getComposerLoader();
} }
/** private static function app(): Application
* Set up global error and exception handlers {
*/ throw new \RuntimeException(
protected static function setupErrorHandlers(): void { 'Server class is deprecated and no longer functional. ' .
// Convert PHP errors to exceptions 'Update your code to use Application class with proper dependency injection. ' .
set_error_handler(function ($errno, $errstr, $errfile, $errline) { 'See the migration guide for details.'
// 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 '<pre>Uncaught Exception: ' . $exception . '</pre>';
} 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 '<pre>' . $message . '</pre>';
} 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());
}
} }
} }

View File

@@ -4,7 +4,7 @@ namespace KTXC\Service;
use KTXC\Identity\Provider\DefaultIdentityProvider; use KTXC\Identity\Provider\DefaultIdentityProvider;
use KTXC\Models\Identity\User; use KTXC\Models\Identity\User;
use KTXC\Server; use Psr\Container\ContainerInterface;
use KTXC\SessionTenant; use KTXC\SessionTenant;
/** /**
@@ -18,7 +18,8 @@ class UserManagerService
public function __construct( public function __construct(
private readonly SessionTenant $tenant, private readonly SessionTenant $tenant,
private readonly UserService $userService private readonly UserService $userService,
private readonly ContainerInterface $container
) { ) {
// Register the default identity provider // Register the default identity provider
$this->providerRegister('default', DefaultIdentityProvider::class); $this->providerRegister('default', DefaultIdentityProvider::class);
@@ -51,8 +52,7 @@ class UserManagerService
$providerClass = $this->availableIdentityProviders[$identifier]; $providerClass = $this->availableIdentityProviders[$identifier];
try { try {
// Server::get automatically detects context from calling object $providerInstance = $this->container->get($providerClass);
$providerInstance = Server::runtimeContainer()->get($providerClass);
// Cache the instance // Cache the instance
$this->cachedIdentityProviders[$identifier] = $providerInstance; $this->cachedIdentityProviders[$identifier] = $providerInstance;

View File

@@ -12,6 +12,10 @@ class SessionTenant
private ?string $domain = null; private ?string $domain = null;
private bool $configured = false; private bool $configured = false;
public function __construct(
private readonly TenantService $tenantService
) {}
/** /**
* Configure the tenant information * Configure the tenant information
* This method is called by the SecurityMiddleware after validation * This method is called by the SecurityMiddleware after validation
@@ -21,8 +25,7 @@ class SessionTenant
if ($this->configured) { if ($this->configured) {
return; return;
} }
$service = Server::runtimeContainer()->get(TenantService::class); $tenant = $this->tenantService->fetchByDomain($domain);
$tenant = $service->fetchByDomain($domain);
if ($tenant) { if ($tenant) {
$this->domain = $domain; $this->domain = $domain;
$this->tenant = $tenant; $this->tenant = $tenant;

View File

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

View File

@@ -5,7 +5,7 @@
<!-- ---------------------------------------------- --> <!-- ---------------------------------------------- -->
<!-- searchbar --> <!-- searchbar -->
<!-- ---------------------------------------------- --> <!-- ---------------------------------------------- -->
<v-text-field persistent-placeholder placeholder="Ctrl + k" color="primary" variant="outlined" hide-details density="compact"> <v-text-field persistent-placeholder placeholder="Ctrl + k" color="primary" variant="outlined" hide-details density="compact" autocomplete="off">
<template v-slot:prepend-inner> <template v-slot:prepend-inner>
<v-icon size="small" color="lightText">mdi-magnify</v-icon> <v-icon size="small" color="lightText">mdi-magnify</v-icon>
</template> </template>

View File

@@ -62,6 +62,10 @@ export async function initializeModules(app: App): Promise<void> {
const loadPromises: Promise<void>[] = []; const loadPromises: Promise<void>[] = [];
for (const [moduleId, moduleInfo] of Object.entries(availableModules)) { 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) { if (moduleInfo.handle && moduleInfo.boot && !moduleInfo.booted) {
const moduleHandle = moduleInfo.handle; const moduleHandle = moduleInfo.handle;
const moduleUrl = `/modules/${moduleInfo.handle}/${moduleInfo.boot}`; const moduleUrl = `/modules/${moduleInfo.handle}/${moduleInfo.boot}`;