modulesRoot = rtrim($modulesRoot, '/'); } /** * Register the autoloader */ public function register(): void { spl_autoload_register([$this, 'loadClass']); } /** * Unregister the autoloader */ public function unregister(): void { spl_autoload_unregister([$this, 'loadClass']); } /** * Scan the modules directory and build a map of namespaces to folder paths * This is called lazily on the first KTXM class request */ private function scan(): void { if ($this->scanned) { return; } $this->namespaceMap = []; if (!is_dir($this->modulesRoot)) { $this->scanned = true; return; } $moduleDirs = glob($this->modulesRoot . '/*', GLOB_ONLYDIR); foreach ($moduleDirs as $moduleDir) { $moduleFile = $moduleDir . '/lib/Module.php'; if (!file_exists($moduleFile)) { continue; } // Extract the namespace from Module.php $namespace = $this->extractNamespace($moduleFile); if ($namespace) { $this->namespaceMap[$namespace] = basename($moduleDir); } } $this->scanned = true; } /** * Load a class by its fully qualified name * * @param string $className Fully qualified class name (e.g., KTXM\ContactsManager\Module) * @return bool True if the class was loaded, false otherwise */ public function loadClass(string $className): bool { try { // Only handle classes in the KTXM namespace if (!str_starts_with($className, 'KTXM\\')) { return false; } // Extract the namespace segment (e.g., ContactsManager from KTXM\ContactsManager\Module) $parts = explode('\\', $className); if (count($parts) < 2) { $this->logError("Invalid class name format: $className (expected at least 2 namespace parts)"); return false; } $namespaceSegment = $parts[1]; // Check if we already have a mapping for this namespace if (!isset($this->namespaceMap[$namespaceSegment])) { // Scan only if we haven't scanned yet (this happens once, on first module access) if (!$this->scanned) { $this->scan(); // Check again after scanning if (!isset($this->namespaceMap[$namespaceSegment])) { $this->logError("No module found for namespace segment: $namespaceSegment (class: $className)"); return false; } } else { $this->logError("Module not found after scan: $namespaceSegment (class: $className)"); return false; } } $folderName = $this->namespaceMap[$namespaceSegment]; // Reconstruct the relative path // KTXM\ContactsManager\Module -> contacts_manager/lib/Module.php // KTXM\ContactsManager\Something -> contacts_manager/lib/Something.php $relativePath = 'lib/' . implode('/', array_slice($parts, 2)) . '.php'; $filePath = $this->modulesRoot . '/' . $folderName . '/' . $relativePath; if (file_exists($filePath)) { require_once $filePath; return true; } $this->logError("File not found for class $className at path: $filePath"); return false; } catch (\Throwable $e) { $this->logError("Exception in ModuleAutoloader while loading $className: " . $e->getMessage(), [ 'exception' => $e, 'file' => $e->getFile(), 'line' => $e->getLine(), ]); return false; } } /** * Log an error from the autoloader * * @param string $message Error message * @param array $context Additional context */ private function logError(string $message, array $context = []): void { // Log to PHP error log error_log('[ModuleAutoloader] ' . $message); if (!empty($context)) { error_log('[ModuleAutoloader Context] ' . json_encode($context)); } } /** * Extract namespace from a Module.php file * * @param string $filePath Path to the Module.php file (at modules/{handle}/lib/Module.php) * @return string|null The namespace segment (e.g., 'ContactsManager') */ private function extractNamespace(string $filePath): ?string { if (!file_exists($filePath)) { return null; } $content = file_get_contents($filePath); if ($content === false) { return null; } // Match namespace declaration: namespace KTXM\; if (preg_match('/^\s*namespace\s+KTXM\\\\([a-zA-Z0-9_]+)\s*;/m', $content, $matches)) { return $matches[1]; } return null; } }