01ed0f3080
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
506 lines
18 KiB
PHP
506 lines
18 KiB
PHP
<?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\Application\KernelOptions;
|
|
use KTXC\Application\Execution\ExecutionDescriptor;
|
|
use KTXC\Application\Execution\ExecutionOutcome;
|
|
use KTXC\Application\Execution\ExecutionRunner;
|
|
use KTXC\Application\Execution\ExecutionRunnerInterface;
|
|
use KTXC\Application\Execution\ExecutionScope;
|
|
use KTXC\Application\Execution\TerminationReport;
|
|
use KTXC\Context\IdentityContext;
|
|
use KTXC\Context\IdentityContextInterface;
|
|
use KTXC\Context\TenantContext;
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Injection\Builder;
|
|
use KTXC\Injection\Container;
|
|
use Psr\Container\ContainerInterface;
|
|
use KTXC\Module\ModuleManager;
|
|
use Psr\Log\LoggerInterface;
|
|
use KTXC\Logger\LoggerFactory;
|
|
use KTXC\Logger\TenantAwareLogger;
|
|
use KTXF\Event\DeferredEventProcessorInterface;
|
|
use KTXF\Event\EventDispatcher;
|
|
use KTXF\Event\EventDispatcherInterface;
|
|
use KTXF\Event\EventListenerRegistrarInterface;
|
|
use KTXF\Event\EventListenerRegistry;
|
|
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 implements KernelInterface
|
|
{
|
|
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;
|
|
private bool $errorHandlerInstalled = false;
|
|
private ?ExecutionScope $activeScope = null;
|
|
private ?ExecutionRunnerInterface $runner = null;
|
|
|
|
private array $config;
|
|
|
|
public function __construct(
|
|
private readonly KernelOptions $options,
|
|
array $config = [],
|
|
) {
|
|
$this->config = $config;
|
|
}
|
|
|
|
public function __clone()
|
|
{
|
|
$this->initialized = false;
|
|
$this->booted = false;
|
|
$this->container = null;
|
|
$this->runner = 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 from config (driver + level-filter; per-tenant wrapping applied later in DI)
|
|
$this->logger = LoggerFactory::create(
|
|
$this->config,
|
|
$this->options->paths->project,
|
|
);
|
|
|
|
$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;
|
|
});
|
|
|
|
$this->errorHandlerInstalled = true;
|
|
|
|
// 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);
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
public function boot(): void
|
|
{
|
|
if (!$this->initialized) {
|
|
$this->initialize();
|
|
}
|
|
|
|
if (!$this->booted) {
|
|
/** @var ModuleManager $moduleManager */
|
|
$moduleManager = $this->container->get(ModuleManager::class);
|
|
$moduleManager->modulesBoot();
|
|
$this->container
|
|
->get(EventListenerRegistry::class)
|
|
->freeze($this->container);
|
|
|
|
$this->booted = true;
|
|
}
|
|
}
|
|
|
|
public function executionRunner(): ExecutionRunnerInterface
|
|
{
|
|
return $this->runner ??= new ExecutionRunner($this);
|
|
}
|
|
|
|
public function reboot(): void
|
|
{
|
|
$this->shutdown();
|
|
$this->boot();
|
|
}
|
|
|
|
public function shutdown(): void
|
|
{
|
|
if (false === $this->initialized) {
|
|
return;
|
|
}
|
|
if ($this->activeScope !== null && !$this->activeScope->terminated()) {
|
|
throw new \LogicException('Cannot shut down the kernel while an execution scope is active.');
|
|
}
|
|
|
|
$this->initialized = false;
|
|
$this->booted = false;
|
|
$this->container = null;
|
|
if ($this->errorHandlerInstalled) {
|
|
restore_error_handler();
|
|
$this->errorHandlerInstalled = false;
|
|
}
|
|
}
|
|
|
|
public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope
|
|
{
|
|
if (!$this->booted) {
|
|
$this->boot();
|
|
}
|
|
if ($this->activeScope !== null) {
|
|
throw new \LogicException('The kernel already has an active execution scope.');
|
|
}
|
|
|
|
$scope = new ExecutionScope(
|
|
$descriptor,
|
|
\KTXC\Application\Execution\ExecutionContext::fromDescriptor($descriptor),
|
|
$this->container->get(TenantContext::class),
|
|
$this->container->get(IdentityContext::class),
|
|
);
|
|
$this->container
|
|
->get(DeferredEventProcessorInterface::class)
|
|
->beginExecution($descriptor->executionId);
|
|
$this->activeScope = $scope;
|
|
|
|
return $scope;
|
|
}
|
|
|
|
public function terminateExecution(
|
|
ExecutionScope $scope,
|
|
ExecutionOutcome $outcome,
|
|
): TerminationReport
|
|
{
|
|
if ($scope->terminated()) {
|
|
return new TerminationReport();
|
|
}
|
|
if ($this->activeScope !== $scope) {
|
|
throw new \LogicException('Cannot terminate a scope that is not active.');
|
|
}
|
|
|
|
$processed = 0;
|
|
$remaining = 0;
|
|
$deadlineExceeded = false;
|
|
$limitExceeded = false;
|
|
$failures = [];
|
|
|
|
try {
|
|
if ($this->container && $this->container->has(DeferredEventProcessorInterface::class)) {
|
|
$result = $this->container
|
|
->get(DeferredEventProcessorInterface::class)
|
|
->processDeferred($scope->descriptor->executionId);
|
|
$processed = $result->processed;
|
|
$remaining = $result->remaining;
|
|
$deadlineExceeded = $result->deadlineExceeded;
|
|
$limitExceeded = $result->limitExceeded;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$failures[] = $e;
|
|
try {
|
|
$this->container
|
|
?->get(DeferredEventProcessorInterface::class)
|
|
->discardDeferred($scope->descriptor->executionId);
|
|
} catch (\Throwable $discardError) {
|
|
$failures[] = $discardError;
|
|
}
|
|
$this->logger?->error('Deferred event processing failed.', [
|
|
'exception' => $e,
|
|
'execution_id' => $scope->descriptor->executionId,
|
|
'runtime' => $scope->descriptor->runtime->value,
|
|
]);
|
|
} finally {
|
|
if ($this->container !== null) {
|
|
foreach ([
|
|
EphemeralCacheInterface::class,
|
|
PersistentCacheInterface::class,
|
|
BlobCacheInterface::class,
|
|
] as $cacheType) {
|
|
$cache = $this->container->get($cacheType);
|
|
$cache->setUserContext(null);
|
|
$cache->setTenantContext(null);
|
|
}
|
|
}
|
|
$scope->dispose();
|
|
$scope->markTerminated();
|
|
$this->activeScope = null;
|
|
}
|
|
|
|
return new TerminationReport(
|
|
deferredProcessed: $processed,
|
|
deferredRemaining: $remaining,
|
|
failures: $failures,
|
|
deadlineExceeded: $deadlineExceeded,
|
|
limitExceeded: $limitExceeded,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the kernel parameters.
|
|
*
|
|
* @return array<string, array|bool|string|int|float|\UnitEnum|null>
|
|
*/
|
|
protected function parameters(): array
|
|
{
|
|
$projectDir = $this->options->paths->project;
|
|
$cacheDir = $this->options->paths->cache($this->environment());
|
|
$logsDir = $this->options->paths->logs();
|
|
|
|
return [
|
|
'kernel.project_dir' => realpath($projectDir) ?: $projectDir,
|
|
'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($cacheDir) ?: $cacheDir,
|
|
'kernel.cache_dir' => realpath($cacheDir) ?: $cacheDir,
|
|
'kernel.logs_dir' => realpath($logsDir) ?: $logsDir,
|
|
'kernel.charset' => 'UTF-8',
|
|
];
|
|
}
|
|
|
|
public function environment(): string
|
|
{
|
|
return $this->options->environment;
|
|
}
|
|
|
|
public function debug(): bool
|
|
{
|
|
return $this->options->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;
|
|
}
|
|
|
|
/**
|
|
* 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->options->paths->project;
|
|
$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),
|
|
|
|
TenantContextInterface::class => \DI\get(TenantContext::class),
|
|
IdentityContextInterface::class => \DI\get(IdentityContext::class),
|
|
|
|
LoggerInterface::class => function (ContainerInterface $c) use ($projectDir) {
|
|
$logConfig = $this->config['log'] ?? [];
|
|
|
|
$logDir = $logConfig['path'] ?? ($projectDir . '/var/log');
|
|
$channel = $logConfig['channel'] ?? 'app';
|
|
$level = $logConfig['level'] ?? 'debug';
|
|
$perTenant = (bool) ($logConfig['per_tenant'] ?? false);
|
|
|
|
return new TenantAwareLogger(
|
|
$this->logger,
|
|
$c->get(TenantContextInterface::class),
|
|
$logDir,
|
|
$channel,
|
|
$level,
|
|
$perTenant,
|
|
);
|
|
},
|
|
|
|
EventDispatcherInterface::class => \DI\get(EventDispatcher::class),
|
|
DeferredEventProcessorInterface::class => \DI\get(EventDispatcher::class),
|
|
EventListenerRegistrarInterface::class => \DI\get(EventListenerRegistry::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(TenantContextInterface::class)) {
|
|
$tenantContext = $c->get(TenantContextInterface::class);
|
|
$cache->setTenantContext($tenantContext->identifier());
|
|
}
|
|
if ($c->has(IdentityContextInterface::class)) {
|
|
$identityContext = $c->get(IdentityContextInterface::class);
|
|
$cache->setUserContext($identityContext->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(TenantContextInterface::class)) {
|
|
$tenantContext = $c->get(TenantContextInterface::class);
|
|
$cache->setTenantContext($tenantContext->identifier());
|
|
}
|
|
if ($c->has(IdentityContextInterface::class)) {
|
|
$identityContext = $c->get(IdentityContextInterface::class);
|
|
$cache->setUserContext($identityContext->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(TenantContextInterface::class)) {
|
|
$tenantContext = $c->get(TenantContextInterface::class);
|
|
$cache->setTenantContext($tenantContext->identifier());
|
|
}
|
|
if ($c->has(IdentityContextInterface::class)) {
|
|
$identityContext = $c->get(IdentityContextInterface::class);
|
|
$cache->setUserContext($identityContext->identifier());
|
|
}
|
|
|
|
return $cache;
|
|
},
|
|
]);
|
|
}
|
|
|
|
}
|