root = sys_get_temp_dir() . '/xc_vm_psr4test_' . bin2hex(random_bytes(6)); mkdir($this->root . '/modules', 0775, true); } protected function tearDown(): void { $this->removeTree($this->root); ServiceContainer::resetInstance(); Router::resetInstance(); NavbarRegistry::reset(); EventDispatcher::resetInstance(); } public function testSubNamespaceClassResolvesViaPsr4(): void { // Module 'psr4alpha' with a sub-namespaced class at Service/AlphaService.php. $this->createModule('psr4alpha'); $this->writeSubClass('psr4alpha', 'Service', 'AlphaService', 'alpha-service'); $loader = new ModuleLoader(); $loader->loadAll($this->root . '/modules'); $this->assertTrue($loader->isLoaded('psr4alpha')); $fqcn = 'XcVm\\Module\\Psr4alpha\\Service\\AlphaService'; // Autoload ON: proves the module's PSR-4 autoloader maps the FQCN onto // {modulePath}/Service/AlphaService.php. $this->assertTrue(class_exists($fqcn), 'Sub-namespace class must resolve via PSR-4'); $this->assertSame('alpha-service', constant($fqcn . '::ORIGIN')); } public function testSameShortNameSubClassesDoNotCollide(): void { // One module, two classes both named "Widget" in different sub-namespaces. // The old short-name + glob would load only one file for both FQCNs. $this->createModule('psr4delta'); $this->writeSubClass('psr4delta', 'Service', 'Widget', 'from-service'); $this->writeSubClass('psr4delta', 'Repository', 'Widget', 'from-repository'); $loader = new ModuleLoader(); $loader->loadAll($this->root . '/modules'); $this->assertTrue($loader->isLoaded('psr4delta')); $service = 'XcVm\\Module\\Psr4delta\\Service\\Widget'; $repository = 'XcVm\\Module\\Psr4delta\\Repository\\Widget'; $this->assertTrue(class_exists($service), 'Service\\Widget must resolve'); $this->assertTrue(class_exists($repository), 'Repository\\Widget must resolve'); $this->assertSame('from-service', constant($service . '::ORIGIN')); $this->assertSame('from-repository', constant($repository . '::ORIGIN')); } public function testForeignNamespaceFallsThrough(): void { // A class outside the module namespace must NOT be claimed by the module // autoloader (it returns without requiring anything → no fatal). $this->createModule('psr4omega'); $loader = new ModuleLoader(); $loader->loadAll($this->root . '/modules'); $this->assertFalse( class_exists('XcVm\\Module\\SomethingElse\\Nope', true), 'Foreign namespace must fall through, not be force-resolved' ); } // ── Helpers ─────────────────────────────────────────────────── private function createModule(string $name): void { $dir = $this->root . '/modules/' . $name; mkdir($dir, 0775, true); file_put_contents($dir . '/module.json', json_encode([ 'name' => $name, 'description' => 'psr4 resolver test', 'version' => '1.0.0', 'requires_core' => '>=2.0', 'environment' => 'main', 'dependencies' => [], 'has_navbar' => false, 'has_settings' => false, ])); $pascal = ucfirst($name); $cls = $pascal . 'Module'; $namespace = 'XcVm\\Module\\' . $pascal; file_put_contents($dir . '/' . $cls . '.php', "root . '/modules/' . $name . '/' . $subDir; if (!is_dir($dir)) { mkdir($dir, 0775, true); } file_put_contents($dir . '/' . $class . '.php', "removeTree($path) : unlink($path); } rmdir($dir); } }