diff --git a/README.md b/README.md index 29e07fc..9f84a5a 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,18 @@ php artisan module:filament:make-panel ``` Follow the interactive prompts to create a new panel in your module. +### Scoping resources, clusters, and pages to a panel + +Each module can register one or more Filament panels through `module:make:filament-panel`. Resources, pages, widgets, and clusters belong to a panel through that panel's `PanelProvider` — typically via `discoverResources()`, `discoverPages()`, and `discoverWidgets()` inside the provider's `panel()` method. + +To keep a resource or cluster in a single panel: + +1. Generate the resource/cluster inside the target module (and panel subdirectory, if you use per-panel folders). +2. Ensure only the intended module `*PanelProvider` discovers that directory/namespace. +3. Register `ModulesPlugin` on the main/admin panel so module panel links appear in navigation when `filament-modules.mode` supports panels. + +Filament's own panel discovery rules apply; this package wires modules and panels together but does not override Filament's per-panel registration model. + ### Protecting your resources, pages and widgets (Access Control) - WIP diff --git a/src/Commands/ModuleMakeFilamentThemeCommand.php b/src/Commands/ModuleMakeFilamentThemeCommand.php index 4efad19..d896543 100644 --- a/src/Commands/ModuleMakeFilamentThemeCommand.php +++ b/src/Commands/ModuleMakeFilamentThemeCommand.php @@ -18,6 +18,8 @@ class ModuleMakeFilamentThemeCommand extends MakeThemeCommand public function handle(Filesystem $filesystem): int { + $this->filesystem = $filesystem; + $module = $this->getModule(); $this->call('vendor:publish', [ diff --git a/src/Modules.php b/src/Modules.php index 6f6a796..6d22f5d 100644 --- a/src/Modules.php +++ b/src/Modules.php @@ -91,6 +91,54 @@ public function convertPathToNamespace(string $fullPath): string ->implode('\\'); } + public function findModuleNameForPath(string $path): ?string + { + $normalizedPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); + $modulesPath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, config('modules.paths.modules', base_path('Modules'))); + + $directory = is_file($normalizedPath) ? dirname($normalizedPath) : $normalizedPath; + + while (str($directory)->startsWith($modulesPath) && $directory !== $modulesPath) { + $moduleJsonPath = $directory . DIRECTORY_SEPARATOR . 'module.json'; + + if (is_file($moduleJsonPath)) { + $moduleJson = json_decode((string) file_get_contents($moduleJsonPath), true); + + return is_array($moduleJson) ? ($moduleJson['name'] ?? basename($directory)) : basename($directory); + } + + $parentDirectory = dirname($directory); + + if ($parentDirectory === $directory) { + break; + } + + $directory = $parentDirectory; + } + + return null; + } + + public function resolveClassFromProviderFile(string $providerPath): ?string + { + if (! is_file($providerPath)) { + return null; + } + + $content = file_get_contents($providerPath); + + if ($content === false || ! preg_match('/^namespace\s+([^;]+);/m', $content, $matches)) { + return null; + } + + return trim($matches[1]) . '\\' . basename($providerPath, '.php'); + } + + public function resolveProviderClass(string $providerPath): string + { + return $this->resolveClassFromProviderFile($providerPath) ?? $this->convertPathToNamespace($providerPath); + } + public function execCommand(string $command, ?Command $artisan = null): void { $process = Process::fromShellCommandline($command); diff --git a/src/ModulesPlugin.php b/src/ModulesPlugin.php index 4e75682..8e4c82e 100644 --- a/src/ModulesPlugin.php +++ b/src/ModulesPlugin.php @@ -109,16 +109,22 @@ protected function getModulePanels(): array $pattern = $basePath . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . $appFolder . DIRECTORY_SEPARATOR . 'Providers' . DIRECTORY_SEPARATOR . 'Filament' . DIRECTORY_SEPARATOR . '*.php'; $panelPaths = glob($pattern); - $panelIds = collect($panelPaths)->map(fn ($path) => FilamentModules::convertPathToNamespace($path))->map(function ($class) { - // Get the panel ID and check if it is registered - $id = str($class)->afterLast('\\')->before('PanelProvider')->kebab()->lower(); - // get module it belongs to as well - $moduleName = str($class)->after('Modules\\')->before('\\Providers\\Filament'); - $module = ModuleFacade::find($moduleName); + $panelIds = collect($panelPaths)->map(function ($path) { + $class = FilamentModules::resolveProviderClass($path); + + if (! class_exists($class)) { + return null; + } + + $moduleName = FilamentModules::findModuleNameForPath($path); + $module = $moduleName ? ModuleFacade::find($moduleName) : null; + if (! $module) { return null; } + $id = str($class)->afterLast('\\')->before('PanelProvider')->kebab()->lower(); + return str($id)->prepend('-')->prepend($module->getKebabName()); }); diff --git a/src/ModulesServiceProvider.php b/src/ModulesServiceProvider.php index 12cc1b1..1895530 100644 --- a/src/ModulesServiceProvider.php +++ b/src/ModulesServiceProvider.php @@ -79,11 +79,17 @@ public function attemptToRegisterModuleProviders(): void $providers = array_merge($serviceProviders, $panelProviders); foreach ($providers as $provider) { - $namespace = FilamentModules::convertPathToNamespace($provider); - $module = str($namespace)->before('\Providers\\')->afterLast('\\')->toString(); + $namespace = FilamentModules::resolveProviderClass($provider); + $moduleName = FilamentModules::findModuleNameForPath($provider); + + if (! $moduleName || ! ModuleFacade::isEnabled($moduleName)) { + continue; + } + $className = str($namespace)->afterLast('\\')->toString(); - if (str($className)->startsWith($module) && ModuleFacade::isEnabled($module)) { - // register the module service provider + $moduleStudlyName = str($moduleName)->studly()->toString(); + + if (str($className)->startsWith($moduleStudlyName) && class_exists($namespace)) { $this->app->register($namespace); } } diff --git a/tests/Unit/ModuleProviderResolutionTest.php b/tests/Unit/ModuleProviderResolutionTest.php new file mode 100644 index 0000000..30199c0 --- /dev/null +++ b/tests/Unit/ModuleProviderResolutionTest.php @@ -0,0 +1,79 @@ +createTestModule('CustomNsModule'); + + $providerPath = $module->appPath('Providers' . DIRECTORY_SEPARATOR . 'CustomNamespaceServiceProvider.php'); + $providerDir = dirname($providerPath); + + if (! is_dir($providerDir)) { + mkdir($providerDir, 0755, true); + } + + file_put_contents($providerPath, <<<'PHP' +toBe('MyCompany\\CRM\\Blog\\Providers\\CustomNamespaceServiceProvider'); + expect(FilamentModules::resolveProviderClass($providerPath)) + ->toBe('MyCompany\\CRM\\Blog\\Providers\\CustomNamespaceServiceProvider'); +}); + +test('can find module name from provider path regardless of namespace', function () { + $module = $this->createTestModule('PathLookupModule'); + + $providerPath = $module->appPath('Providers' . DIRECTORY_SEPARATOR . 'BlogServiceProvider.php'); + + expect(FilamentModules::findModuleNameForPath($providerPath))->toBe('PathLookupModule'); +}); + +test('registers providers that declare custom namespaces when enabled', function () { + $module = $this->createTestModule('CustomProviderModule', enabled: true); + + $providerPath = $module->appPath('Providers' . DIRECTORY_SEPARATOR . 'BlogServiceProvider.php'); + $providerDir = dirname($providerPath); + + if (! is_dir($providerDir)) { + mkdir($providerDir, 0755, true); + } + + file_put_contents($providerPath, <<<'PHP' +toBe('CustomProviderModule'); + expect(str($namespace)->afterLast('\\')->startsWith('Blog'))->toBeTrue(); + + require_once $providerPath; + + $this->app->register($namespace); + + expect(collect($this->app->getProviders($namespace)))->not->toBeEmpty(); +}); diff --git a/tests/Unit/ModulesServiceProviderBootTest.php b/tests/Unit/ModulesServiceProviderBootTest.php index 7d9d791..b595e28 100644 --- a/tests/Unit/ModulesServiceProviderBootTest.php +++ b/tests/Unit/ModulesServiceProviderBootTest.php @@ -20,19 +20,19 @@ } file_put_contents($providerPath, <<<'PHP' -