diff --git a/core/lib/Kernel.php b/core/lib/Kernel.php index 4fcfd05..02259e6 100644 --- a/core/lib/Kernel.php +++ b/core/lib/Kernel.php @@ -10,8 +10,6 @@ namespace KTXC; use KTXC\Http\Request\Request; -use KTXC\Http\Response\JsonResponse; -use KTXC\Http\Response\RedirectResponse; use KTXC\Http\Response\Response; use KTXC\Injection\Builder; use KTXC\Injection\Container; @@ -30,8 +28,6 @@ use KTXF\Cache\BlobCacheInterface; use KTXF\Cache\Store\FileEphemeralCache; use KTXF\Cache\Store\FilePersistentCache; use KTXF\Cache\Store\FileBlobCache; -use KTXM\MailManager\Queue\MailQueue; -use KTXM\MailManager\Queue\MailQueueFile; class Kernel { diff --git a/core/lib/Module/ModuleAutoloader.php b/core/lib/Module/ModuleAutoloader.php index 30f50c6..ea840fc 100644 --- a/core/lib/Module/ModuleAutoloader.php +++ b/core/lib/Module/ModuleAutoloader.php @@ -2,6 +2,8 @@ namespace KTXC\Module; +use KTXC\Server; + /** * Custom autoloader for modules that allows PascalCase namespaces * with lowercase folder names. @@ -70,6 +72,17 @@ class ModuleAutoloader } } + // Register module namespaces with Composer ClassLoader + $composerLoader = Server::getComposerLoader(); + if ($composerLoader !== null) { + foreach ($this->namespaceMap as $namespace => $folderName) { + $composerLoader->addPsr4( + 'KTXM\\' . $namespace . '\\', + $this->modulesRoot . '/' . $folderName . '/lib/' + ); + } + } + $this->scanned = true; } diff --git a/core/lib/Module/ModuleManager.php b/core/lib/Module/ModuleManager.php index 297e2de..0b8a979 100644 --- a/core/lib/Module/ModuleManager.php +++ b/core/lib/Module/ModuleManager.php @@ -331,6 +331,9 @@ class ModuleManager public function moduleInstance(string $handle, ?string $namespace = null): ?ModuleInstanceInterface { + // Load module's vendor autoloader if it exists + $this->loadModuleVendor($handle); + // Return from cache if already instantiated if (isset($this->moduleInstances[$handle])) { return $this->moduleInstances[$handle]; @@ -429,6 +432,37 @@ class ModuleManager } } + /** + * Load a module's vendor autoloader if it has dependencies + * + * @param string $handle Module handle + * @throws Exception If module has dependencies but vendor directory is missing + */ + private function loadModuleVendor(string $handle): void + { + $moduleDir = $this->serverRoot . '/modules/' . $handle; + $composerJson = $moduleDir . '/composer.json'; + $vendorAutoload = $moduleDir . '/lib/vendor/autoload.php'; + + // Check if module has a composer.json with dependencies + if (file_exists($composerJson)) { + $composerData = json_decode(file_get_contents($composerJson), true); + $hasDependencies = !empty($composerData['require']) && count($composerData['require']) > 1; // More than just PHP + + if ($hasDependencies) { + if (file_exists($vendorAutoload)) { + require_once $vendorAutoload; + $this->logger->debug("Loaded vendor autoloader for module: {$handle}"); + } else { + throw new Exception( + "Module '{$handle}' declares dependencies in composer.json but vendor directory is missing. " + . "Run 'composer install' in {$moduleDir}" + ); + } + } + } + } + private function studly(string $value): string { $value = str_replace(['-', '_'], ' ', strtolower($value)); diff --git a/core/lib/Resolver.php b/core/lib/Resolver.php deleted file mode 100644 index 41e08db..0000000 --- a/core/lib/Resolver.php +++ /dev/null @@ -1,253 +0,0 @@ -resolveWithContext($className, []); - } - - /** - * Resolve and instantiate a class with specific context instances - */ - public function resolveWithContext(string $className, array $contextInstances = []): object - { - // Store context instances temporarily - $originalContext = $this->contextInstances; - $this->contextInstances = array_merge($this->contextInstances, $contextInstances); - - try { - // Check cache first (but not when we have context overrides) - if (empty($contextInstances) && isset($this->instanceCache[$className])) { - return $this->instanceCache[$className]; - } - - // Check if class exists - if (!class_exists($className)) { - throw new \InvalidArgumentException("Class {$className} does not exist"); - } - - $reflectionClass = new ReflectionClass($className); - - // If class cannot be instantiated - if (!$reflectionClass->isInstantiable()) { - throw new \InvalidArgumentException("Class {$className} is not instantiable"); - } - - $constructor = $reflectionClass->getConstructor(); - - // If no constructor, just instantiate - if ($constructor === null) { - $instance = new $className(); - if (empty($contextInstances)) { - $this->instanceCache[$className] = $instance; - } - return $instance; - } - - // Resolve constructor dependencies - $dependencies = []; - foreach ($constructor->getParameters() as $parameter) { - $dependencies[] = $this->resolveParameter($parameter); - } - - // Create instance with resolved dependencies - $instance = $reflectionClass->newInstanceArgs($dependencies); - - // Only cache if no context overrides - if (empty($contextInstances)) { - $this->instanceCache[$className] = $instance; - } - - return $instance; - } finally { - // Restore original context - $this->contextInstances = $originalContext; - } - } - - /** - * Resolve a single parameter dependency - */ - private function resolveParameter(ReflectionParameter $parameter): mixed - { - $type = $parameter->getType(); - - // If no type hint, check if it has a default value - if ($type === null) { - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - throw new \InvalidArgumentException("Cannot resolve parameter {$parameter->getName()} without type hint"); - } - - // Handle union types (PHP 8+) - if ($type instanceof \ReflectionUnionType) { - throw new \InvalidArgumentException("Union types are not supported for parameter {$parameter->getName()}"); - } - - $typeName = $type->getName(); - - // Check if we have a context instance for this type - if (isset($this->contextInstances[$typeName])) { - return $this->contextInstances[$typeName]; - } - - // Check global context - if (isset(self::$globalContext[$typeName])) { - return self::$globalContext[$typeName]; - } - - // Handle built-in types - if ($type->isBuiltin()) { - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - throw new \InvalidArgumentException("Cannot resolve built-in type {$typeName} for parameter {$parameter->getName()}"); - } - - // Always try to get from container first for all non-builtin types - try { - $container = Server::getContainer(); - if ($container->has($typeName)) { - return $container->get($typeName); - } - } catch (\Exception $e) { - // Fall through to manual resolution - } - - // Only try manual resolution for classes in our namespace - if (strpos($typeName, 'KTXC\\') === 0) { - try { - return $this->resolve($typeName); - } catch (\Exception $e) { - // If still can't resolve and has default value, use it - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - // If parameter is nullable, return null - if ($parameter->allowsNull()) { - return null; - } - - throw new \InvalidArgumentException("Cannot resolve dependency {$typeName} for parameter {$parameter->getName()}: " . $e->getMessage()); - } - } - - // For non-Ktrix classes that aren't in the container, fail gracefully - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - if ($parameter->allowsNull()) { - return null; - } - - throw new \InvalidArgumentException("Cannot resolve external dependency {$typeName} for parameter {$parameter->getName()}. This service should be registered in the container."); - } - - /** - * Resolve and instantiate a class with automatic context detection - */ - public function resolveWithAutoContext(string $className, object $contextSource = null): object - { - $contextInstances = []; - - if ($contextSource !== null) { - // Use reflection to get all properties of the context source - $reflection = new ReflectionClass($contextSource); - $properties = $reflection->getProperties(); - - foreach ($properties as $property) { - $property->setAccessible(true); - $value = $property->getValue($contextSource); - - if (is_object($value)) { - $contextInstances[get_class($value)] = $value; - } - } - } - - return $this->resolveWithContext($className, $contextInstances); - } - - /** - * Smart resolve - automatically finds dependencies from the call stack - */ - public function smartResolve(string $className): object - { - $contextInstances = []; - - // Get the call stack to find potential context sources - $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 10); - - foreach ($backtrace as $frame) { - if (isset($frame['object']) && is_object($frame['object'])) { - $sourceObject = $frame['object']; - $reflection = new ReflectionClass($sourceObject); - $properties = $reflection->getProperties(); - - foreach ($properties as $property) { - $property->setAccessible(true); - $value = $property->getValue($sourceObject); - - if (is_object($value)) { - $contextInstances[get_class($value)] = $value; - } - } - } - } - - return $this->resolveWithContext($className, $contextInstances); - } - - /** - * Clear the instance cache - */ - public function clearCache(): void - { - $this->instanceCache = []; - } - - /** - * Check if a class is cached - */ - public function isCached(string $className): bool - { - return isset($this->instanceCache[$className]); - } -} diff --git a/core/lib/Server.php b/core/lib/Server.php index 729e2ea..9d723e3 100644 --- a/core/lib/Server.php +++ b/core/lib/Server.php @@ -18,6 +18,7 @@ class Server public const ENVIRONMENT_PROD = 'prod'; protected static $kernel; + protected static $composerLoader; public static function run() { // Set up global error handler before anything else @@ -67,6 +68,20 @@ class Server return self::$kernel->folderRoot() . '/modules'; } + /** + * 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; + } + /** * Set up global error and exception handlers */ diff --git a/core/lib/index.php b/core/lib/index.php index f798d49..82dd86c 100644 --- a/core/lib/index.php +++ b/core/lib/index.php @@ -3,7 +3,9 @@ use KTXC\Server; use KTXC\Module\ModuleAutoloader; -require_once __DIR__ . '../../vendor/autoload.php'; +// Capture Composer ClassLoader instance +$composerLoader = require_once __DIR__ . '../../vendor/autoload.php'; +Server::setComposerLoader($composerLoader); // Register custom module autoloader for lazy loading $moduleAutoloader = new ModuleAutoloader(__DIR__ . '/../modules');