312 lines
8.3 KiB
PHP
312 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace KTXC;
|
|
|
|
use KTXC\Http\Request\Request;
|
|
use KTXC\Http\Response\Response;
|
|
use KTXC\Module\ModuleAutoloader;
|
|
use KTXC\Module\ModuleManager;
|
|
use KTXF\Module\ModuleConsoleInterface;
|
|
use Psr\Container\ContainerInterface;
|
|
use Symfony\Component\Console\Application as ConsoleApplication;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\LazyCommand;
|
|
|
|
/**
|
|
* Server class - entry point for the framework
|
|
* Handles configuration loading and kernel lifecycle
|
|
*/
|
|
class Server
|
|
{
|
|
private static $composerLoader = null;
|
|
private static ?self $instance = null;
|
|
|
|
private Kernel $kernel;
|
|
private array $config;
|
|
private string $rootDir;
|
|
|
|
public function __construct(string $rootDir, ?string $environment = null, ?bool $debug = null)
|
|
{
|
|
self::$instance = $this;
|
|
|
|
$this->rootDir = $this->resolveProjectRoot($rootDir);
|
|
|
|
// Load configuration
|
|
$this->config = $this->loadConfig();
|
|
|
|
// Determine environment and debug mode
|
|
$environment = $environment ?? $this->config['environment'] ?? 'prod';
|
|
$debug = $debug ?? $this->config['debug'] ?? false;
|
|
|
|
// Create kernel with configuration
|
|
$this->kernel = new Kernel($environment, $debug, $this->config, $rootDir);
|
|
|
|
// Register module autoloader for both HTTP and CLI contexts
|
|
$moduleAutoloader = new ModuleAutoloader($this->moduleDir());
|
|
$moduleAutoloader->register();
|
|
}
|
|
|
|
/**
|
|
* Run the application - handle incoming request and send response
|
|
*/
|
|
public function runHttp(): void
|
|
{
|
|
try {
|
|
$request = Request::createFromGlobals();
|
|
$response = $this->handle($request);
|
|
$response->send();
|
|
$this->terminate();
|
|
} catch (\Throwable $e) {
|
|
// Last resort error handling for kernel initialization failures
|
|
error_log('Application error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
|
$content = $this->kernel->debug()
|
|
? '<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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run as a console application (CLI runtime).
|
|
*/
|
|
public function runConsole(): int
|
|
{
|
|
$this->kernel()->boot();
|
|
$container = $this->container();
|
|
|
|
$console = new ConsoleApplication('Ktrix Console', Kernel::VERSION);
|
|
|
|
/** @var ModuleManager $moduleManager */
|
|
$moduleManager = $container->get(ModuleManager::class);
|
|
|
|
foreach ($moduleManager->list() as $module) {
|
|
$instance = $module->instance();
|
|
if (!$instance instanceof ModuleConsoleInterface) {
|
|
continue;
|
|
}
|
|
try {
|
|
foreach ($instance->registerCI() as $commandClass) {
|
|
if (!class_exists($commandClass)) {
|
|
fwrite(STDERR, "Warning: Command class not found: {$commandClass}\n");
|
|
continue;
|
|
}
|
|
$this->registerLazyCommand($console, $container, $commandClass);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
fwrite(STDERR, "Warning: Failed to load commands from module {$module->handle()}: {$e->getMessage()}\n");
|
|
}
|
|
}
|
|
|
|
return $console->run();
|
|
}
|
|
|
|
/**
|
|
* Handle a request
|
|
*/
|
|
public function handle(Request $request): Response
|
|
{
|
|
return $this->kernel->handle($request);
|
|
}
|
|
|
|
/**
|
|
* Terminate the application - process deferred events
|
|
*/
|
|
public function terminate(): void
|
|
{
|
|
$this->kernel->processEvents();
|
|
}
|
|
|
|
/**
|
|
* Get the kernel instance
|
|
*/
|
|
public function kernel(): Kernel
|
|
{
|
|
return $this->kernel;
|
|
}
|
|
|
|
/**
|
|
* Get the container instance
|
|
*/
|
|
public function container(): ContainerInterface
|
|
{
|
|
return $this->kernel->container();
|
|
}
|
|
|
|
/**
|
|
* Get the application root directory
|
|
*/
|
|
public function rootDir(): string
|
|
{
|
|
return $this->rootDir;
|
|
}
|
|
|
|
/**
|
|
* Get the modules directory
|
|
*/
|
|
public function moduleDir(): string
|
|
{
|
|
return $this->rootDir . '/modules';
|
|
}
|
|
|
|
public function varDir(): string
|
|
{
|
|
return $this->rootDir . '/var';
|
|
}
|
|
|
|
public function logDir(): string
|
|
{
|
|
return $this->varDir() . '/logs';
|
|
}
|
|
|
|
/**
|
|
* Get configuration value
|
|
*/
|
|
public function config(?string $key = null, mixed $default = null): mixed
|
|
{
|
|
if ($key === null) {
|
|
return $this->config;
|
|
}
|
|
|
|
// Support dot notation: 'database.uri'
|
|
$keys = explode('.', $key);
|
|
$value = $this->config;
|
|
|
|
foreach ($keys as $k) {
|
|
if (!is_array($value) || !array_key_exists($k, $value)) {
|
|
return $default;
|
|
}
|
|
$value = $value[$k];
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Get environment
|
|
*/
|
|
public function environment(): string
|
|
{
|
|
return $this->kernel->environment();
|
|
}
|
|
|
|
/**
|
|
* Check if debug mode is enabled
|
|
*/
|
|
public function debug(): bool
|
|
{
|
|
return $this->kernel->debug();
|
|
}
|
|
|
|
/**
|
|
* Load configuration from config directory
|
|
*/
|
|
protected function loadConfig(): array
|
|
{
|
|
$configFile = $this->rootDir . '/config/system.php';
|
|
|
|
if (!file_exists($configFile)) {
|
|
error_log('Configuration file not found: ' . $configFile);
|
|
return [];
|
|
}
|
|
|
|
$config = include $configFile;
|
|
|
|
if (!is_array($config)) {
|
|
throw new \RuntimeException('Configuration file must return an array');
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Resolve the project root directory.
|
|
*
|
|
* Some entrypoints may pass the public/ directory or another subdirectory.
|
|
* We walk up the directory tree until we find composer.json.
|
|
*/
|
|
private function resolveProjectRoot(string $startDir): string
|
|
{
|
|
$dir = rtrim($startDir, '/');
|
|
if ($dir === '') {
|
|
return $startDir;
|
|
}
|
|
|
|
// If startDir is a file path, use its directory.
|
|
if (is_file($dir)) {
|
|
$dir = dirname($dir);
|
|
}
|
|
|
|
$current = $dir;
|
|
while (true) {
|
|
if (is_file($current . '/composer.json')) {
|
|
return $current;
|
|
}
|
|
|
|
$parent = dirname($current);
|
|
if ($parent === $current) {
|
|
// Reached filesystem root
|
|
return $dir;
|
|
}
|
|
$current = $parent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the Composer ClassLoader instance
|
|
*/
|
|
public static function setComposerLoader($loader): void
|
|
{
|
|
self::$composerLoader = $loader;
|
|
}
|
|
|
|
/**
|
|
* Get the Composer ClassLoader instance
|
|
*/
|
|
public static function getComposerLoader()
|
|
{
|
|
return self::$composerLoader;
|
|
}
|
|
|
|
/**
|
|
* Get the current Application instance
|
|
*/
|
|
public static function getInstance(): ?self
|
|
{
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Register a single command via lazy loading using its #[AsCommand] attribute.
|
|
*/
|
|
private function registerLazyCommand(
|
|
ConsoleApplication $console,
|
|
ContainerInterface $container,
|
|
string $commandClass
|
|
): void {
|
|
try {
|
|
$ref = new \ReflectionClass($commandClass);
|
|
$attrs = $ref->getAttributes(AsCommand::class);
|
|
|
|
if (empty($attrs)) {
|
|
fwrite(STDERR, "Warning: Command {$commandClass} missing #[AsCommand] attribute\n");
|
|
return;
|
|
}
|
|
|
|
$attr = $attrs[0]->newInstance();
|
|
$console->add(new LazyCommand(
|
|
$attr->name,
|
|
[],
|
|
$attr->description ?? '',
|
|
$attr->hidden ?? false,
|
|
fn() => $container->get($commandClass)
|
|
));
|
|
} catch (\Throwable $e) {
|
|
fwrite(STDERR, "Warning: Failed to register command {$commandClass}: {$e->getMessage()}\n");
|
|
}
|
|
}
|
|
}
|