diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index 0353ae5..c1868fc 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -11,6 +11,8 @@ class ModuleRegistry { protected ?Collection $modules = null; + protected ?array $namespace_index = null; + public function __construct( protected string $modules_path, protected Closure $modules_loader, @@ -44,18 +46,16 @@ public function moduleForPathOrFail(string $path): ModuleConfig public function moduleForClass(string $fqcn): ?ModuleConfig { - return $this->modules()->first(function(ModuleConfig $module) use ($fqcn) { - foreach ($module->namespaces as $namespace) { - if (Str::startsWith($fqcn, $namespace)) { - return true; - } + foreach ($this->namespaceIndex() as $namespace => $module) { + if (Str::startsWith($fqcn, $namespace)) { + return $module; } - - return false; - }); + } + + return null; } - /** @return Collection */ + /** @return Collection */ public function modules(): Collection { return $this->modules ??= call_user_func($this->modules_loader); @@ -64,9 +64,23 @@ public function modules(): Collection public function reload(): Collection { $this->modules = null; + $this->namespace_index = null; return $this->modules(); } + + /** + * Namespaces sorted descending so the most-specific prefix wins on first match. + * + * @return array + */ + protected function namespaceIndex(): array + { + return $this->namespace_index ??= $this->modules() + ->flatMap(fn(ModuleConfig $module) => $module->namespaces->mapWithKeys(fn(string $namespace) => [$namespace => $module])) + ->sortKeysDesc() + ->all(); + } protected function extractModuleNameFromPath(string $path): string { diff --git a/tests/ModuleRegistryTest.php b/tests/ModuleRegistryTest.php index 36e2964..08c99aa 100644 --- a/tests/ModuleRegistryTest.php +++ b/tests/ModuleRegistryTest.php @@ -2,6 +2,7 @@ namespace InterNACHI\Modular\Tests; +use Illuminate\Support\Collection; use InterNACHI\Modular\Support\ModuleConfig; use InterNACHI\Modular\Support\ModuleRegistry; use InterNACHI\Modular\Tests\Concerns\WritesToAppFilesystem; @@ -9,7 +10,66 @@ class ModuleRegistryTest extends TestCase { use WritesToAppFilesystem; - + + public function test_module_for_class_prefers_longest_matching_namespace(): void + { + $general = new ModuleConfig('general', '/tmp/general', new Collection([ + '/tmp/general/src' => 'App\\General\\', + '/tmp/general/tests' => 'App\\General\\Tests', + ])); + + $specific = new ModuleConfig('specific', '/tmp/specific', new Collection([ + '/tmp/general-specific/src' => 'App\\General\\Specific\\', + '/tmp/general-specific/tests' => 'App\\General\\Specific\\Tests', + ])); + + $loader_orders = [ + [$general, $specific], + [$specific, $general], + ]; + + $specific_model = 'App\\General\\Specific\\Models\\Thing'; + $specific_test = 'App\\General\\Specific\\Tests\\ThingTest'; + $general_model = 'App\\General\\Models\\Thing'; + $general_test = 'App\\General\\Tests\\ThingTest'; + + foreach ($loader_orders as $modules) { + $registry = new ModuleRegistry( + modules_path: '/tmp', + modules_loader: fn() => Collection::make($modules)->keyBy('name'), + ); + + $this->assertEquals( + $specific, + $registry->moduleForClass($specific_model), + 'Expected the more-specific module to win regardless of registration order (primary namespace)', + ); + + $this->assertEquals( + $specific, + $registry->moduleForClass($specific_test), + 'Expected the more-specific module to win regardless of registration order (secondary namespace)', + ); + + $this->assertEquals( + $general, + $registry->moduleForClass($general_model), + 'Expected the general module to return when a more-specific namespace does not match (primary namespace)', + ); + + $this->assertEquals( + $general, + $registry->moduleForClass($general_test), + 'Expected the general module to return when a more-specific namespace does not match (secondary namespace)', + ); + + $this->assertNull( + $registry->moduleForClass('Unrelated\\Thing'), + 'Expected unrelated class not to match any module regardless of order' + ); + } + } + public function test_it_resolves_modules(): void { $this->makeModule('test-module');