Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

494
core/lib/Kernel.php Normal file
View File

@@ -0,0 +1,494 @@
<?php
/*
* This file is part of the Symfony package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
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 KTXF\Event\EventBus;
use KTXF\Cache\EphemeralCacheInterface;
use KTXF\Cache\PersistentCacheInterface;
use KTXF\Cache\BlobCacheInterface;
use KTXF\Cache\Store\FileEphemeralCache;
use KTXF\Cache\Store\FilePersistentCache;
use KTXF\Cache\Store\FileBlobCache;
class Kernel
{
public const VERSION = '1.0.0';
public const VERSION_ID = 10000;
public const MAJOR_VERSION = 1;
public const MINOR_VERSION = 0;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
protected bool $initialized = false;
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()
{
$this->initialized = false;
$this->booted = false;
$this->container = null;
}
private function initialize(): void
{
if ($this->debug) {
$this->startTime = microtime(true);
}
if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) {
if (\function_exists('putenv')) {
putenv('SHELL_VERBOSITY=3');
}
$_ENV['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();
$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 '<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
{
if (!$this->initialized) {
$this->initialize();
}
if (!$this->booted) {
/** @var ModuleManager $moduleManager */
$moduleManager = $this->container->get(ModuleManager::class);
$moduleManager->modulesBoot();
// Build middleware pipeline
$this->pipeline = $this->buildMiddlewarePipeline();
$this->booted = true;
}
}
public function reboot(): void
{
$this->shutdown();
$this->boot();
}
public function shutdown(): void
{
if (false === $this->initialized) {
return;
}
$this->initialized = false;
$this->booted = false;
$this->container = null;
}
public function handle(Request $request): Response
{
if (!$this->booted) {
$this->boot();
}
// Use middleware pipeline to handle the request
return $this->pipeline->handle($request);
}
/**
* 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;
}
/**
* Process deferred events at the end of the request
*/
public function processEvents(): void
{
try {
if ($this->container && $this->container->has(EventBus::class)) {
/** @var EventBus $eventBus */
$eventBus = $this->container->get(EventBus::class);
$eventBus->processDeferred();
}
} catch (\Throwable $e) {
error_log('Event processing error: ' . $e->getMessage());
}
}
/**
* Returns the kernel parameters.
*
* @return array<string, array|bool|string|int|float|\UnitEnum|null>
*/
protected function parameters(): array
{
return [
'kernel.project_dir' => realpath($this->folderRoot()) ?: $this->folderRoot(),
'kernel.environment' => $this->environment,
'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%',
'kernel.runtime_mode' => '%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%',
'kernel.runtime_mode.web' => '%env(bool:default::key:web:default:kernel.runtime_mode:)%',
'kernel.runtime_mode.cli' => '%env(not:default:kernel.runtime_mode.web:)%',
'kernel.runtime_mode.worker' => '%env(bool:default::key:worker:default:kernel.runtime_mode:)%',
'kernel.debug' => $this->debug,
'kernel.build_dir' => realpath($this->getBuildDir()) ?: $this->getBuildDir(),
'kernel.cache_dir' => realpath($this->getCacheDir()) ?: $this->getCacheDir(),
'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(),
'kernel.charset' => $this->getCharset(),
];
}
public function environment(): string
{
return $this->environment;
}
public function debug(): bool
{
return $this->debug;
}
public function container(): ContainerInterface
{
if (!$this->container) {
throw new \LogicException('Cannot retrieve the container from a non-booted kernel.');
}
return $this->container;
}
public function getStartTime(): float
{
return $this->debug && null !== $this->startTime ? $this->startTime : -\INF;
}
/**
* Gets the application root dir (path of the project's composer file).
*/
public function folderRoot(): string
{
if (!isset($this->projectDir)) {
$r = new \ReflectionObject($this);
if (!is_file($dir = $r->getFileName())) {
throw new \LogicException(\sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name));
}
$dir = $rootDir = \dirname($dir);
while (!is_file($dir.'/composer.json')) {
if ($dir === \dirname($dir)) {
return $this->projectDir = $rootDir;
}
$dir = \dirname($dir);
}
$this->projectDir = $dir;
}
return $this->projectDir;
}
/**
* Gets the path to the configuration directory.
*/
private function getConfigDir(): string
{
return $this->folderRoot().'/config';
}
public function getCacheDir(): string
{
return $this->folderRoot().'/var/cache/'.$this->environment;
}
public function getBuildDir(): string
{
return $this->getCacheDir();
}
public function getLogDir(): string
{
return $this->folderRoot().'/var/log';
}
public function getCharset(): string
{
return 'UTF-8';
}
/**
* Initializes the service container
*/
protected function initializeContainer(): Container
{
$container = $this->buildContainer();
$container->set('kernel', $this);
return $container;
}
/**
* Builds the service container.
*
* @throws \RuntimeException
*/
protected function buildContainer(): Container
{
$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();
}
protected function configureContainer(Builder $builder): void
{
// Service definitions
$projectDir = $this->folderRoot();
$moduleDir = $projectDir . '/modules';
$environment = $this->environment;
$builder->addDefinitions([
// 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)
EphemeralCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
$storeType = $c->has('cache.ephemeral') ? $c->get('cache.ephemeral') : 'file';
$storeMap = [
'file' => FileEphemeralCache::class,
// 'redis' => RedisEphemeralCache::class,
];
$storeClass = $storeMap[$storeType] ?? $storeType;
if (!class_exists($storeClass)) {
throw new \RuntimeException("Ephemeral cache store not found: {$storeClass}");
}
$cache = new $storeClass($projectDir);
// Set tenant/user context if available
if ($c->has(SessionTenant::class)) {
$tenant = $c->get(SessionTenant::class);
$cache->setTenantContext($tenant->identifier());
}
if ($c->has(SessionIdentity::class)) {
$identity = $c->get(SessionIdentity::class);
$cache->setUserContext($identity->identifier());
}
return $cache;
},
// Persistent Cache - for long-lived data (routes, modules, compiled configs)
PersistentCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
$storeType = $c->has('cache.persistent') ? $c->get('cache.persistent') : 'file';
$storeMap = [
'file' => FilePersistentCache::class,
// 'database' => DatabasePersistentCache::class,
];
$storeClass = $storeMap[$storeType] ?? $storeType;
if (!class_exists($storeClass)) {
throw new \RuntimeException("Persistent cache store not found: {$storeClass}");
}
$cache = new $storeClass($projectDir);
// Set tenant/user context if available
if ($c->has(SessionTenant::class)) {
$tenant = $c->get(SessionTenant::class);
$cache->setTenantContext($tenant->identifier());
}
if ($c->has(SessionIdentity::class)) {
$identity = $c->get(SessionIdentity::class);
$cache->setUserContext($identity->identifier());
}
return $cache;
},
// Blob Cache - for binary/media data (previews, thumbnails)
BlobCacheInterface::class => function(ContainerInterface $c) use ($projectDir) {
$storeType = $c->has('cache.blob') ? $c->get('cache.blob') : 'file';
$storeMap = [
'file' => FileBlobCache::class,
// 's3' => S3BlobCache::class,
];
$storeClass = $storeMap[$storeType] ?? $storeType;
if (!class_exists($storeClass)) {
throw new \RuntimeException("Blob cache store not found: {$storeClass}");
}
$cache = new $storeClass($projectDir);
// Set tenant/user context if available
if ($c->has(SessionTenant::class)) {
$tenant = $c->get(SessionTenant::class);
$cache->setTenantContext($tenant->identifier());
}
if ($c->has(SessionIdentity::class)) {
$identity = $c->get(SessionIdentity::class);
$cache->setUserContext($identity->identifier());
}
return $cache;
},
]);
}
}