kernal clean-up
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Application Configuration
|
||||
'name' => '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',
|
||||
//],
|
||||
|
||||
];
|
||||
|
||||
216
core/lib/Application.php
Normal file
216
core/lib/Application.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
38
core/lib/Http/Middleware/AuthenticationMiddleware.php
Normal file
38
core/lib/Http/Middleware/AuthenticationMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
32
core/lib/Http/Middleware/FirewallMiddleware.php
Normal file
32
core/lib/Http/Middleware/FirewallMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
21
core/lib/Http/Middleware/MiddlewareInterface.php
Normal file
21
core/lib/Http/Middleware/MiddlewareInterface.php
Normal 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;
|
||||
}
|
||||
112
core/lib/Http/Middleware/MiddlewarePipeline.php
Normal file
112
core/lib/Http/Middleware/MiddlewarePipeline.php
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
20
core/lib/Http/Middleware/RequestHandlerInterface.php
Normal file
20
core/lib/Http/Middleware/RequestHandlerInterface.php
Normal 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;
|
||||
}
|
||||
50
core/lib/Http/Middleware/RouterMiddleware.php
Normal file
50
core/lib/Http/Middleware/RouterMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
35
core/lib/Http/Middleware/TenantMiddleware.php
Normal file
35
core/lib/Http/Middleware/TenantMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 '<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) {
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
? '<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';
|
||||
$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 '<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());
|
||||
}
|
||||
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.'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<?php
|
||||
|
||||
use KTXC\Server;
|
||||
use KTXC\Application;
|
||||
use KTXC\Module\ModuleAutoloader;
|
||||
|
||||
// Capture Composer ClassLoader instance
|
||||
$composerLoader = require_once __DIR__ . '../../vendor/autoload.php';
|
||||
Server::setComposerLoader($composerLoader);
|
||||
$composerLoader = require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// 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
|
||||
$moduleAutoloader = new ModuleAutoloader(__DIR__ . '/../modules');
|
||||
$moduleAutoloader = new ModuleAutoloader($app->moduleDir());
|
||||
$moduleAutoloader->register();
|
||||
|
||||
Server::run();
|
||||
$app->run();
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- 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>
|
||||
<v-icon size="small" color="lightText">mdi-magnify</v-icon>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +62,10 @@ export async function initializeModules(app: App): Promise<void> {
|
||||
const loadPromises: Promise<void>[] = [];
|
||||
|
||||
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}`;
|
||||
|
||||
Reference in New Issue
Block a user