Initial Version
This commit is contained in:
195
core/lib/Module/ModuleAutoloader.php
Normal file
195
core/lib/Module/ModuleAutoloader.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Module;
|
||||
|
||||
use KTXC\Application;
|
||||
|
||||
/**
|
||||
* Custom autoloader for modules that allows PascalCase namespaces
|
||||
* with lowercase folder names.
|
||||
*
|
||||
* This breaks from PSR-4 convention to allow:
|
||||
* - Folder: modules/contacts_manager/
|
||||
* - Namespace: KTXM\ContactsManager\
|
||||
*
|
||||
* The autoloader scans lazily - only when a KTXM class is first requested.
|
||||
*/
|
||||
class ModuleAutoloader
|
||||
{
|
||||
private string $modulesRoot;
|
||||
private array $namespaceMap = [];
|
||||
private bool $scanned = false;
|
||||
|
||||
public function __construct(string $modulesRoot)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
||||
// Register module namespaces with Composer ClassLoader
|
||||
$composerLoader = \KTXC\Application::getComposerLoader();
|
||||
if ($composerLoader !== null) {
|
||||
foreach ($this->namespaceMap as $namespace => $folderName) {
|
||||
$composerLoader->addPsr4(
|
||||
'KTXM\\' . $namespace . '\\',
|
||||
$this->modulesRoot . '/' . $folderName . '/lib/'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$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\<Namespace>;
|
||||
if (preg_match('/^\s*namespace\s+KTXM\\\\([a-zA-Z0-9_]+)\s*;/m', $content, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user