Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/Commands/ModuleMakeFilamentThemeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class ModuleMakeFilamentThemeCommand extends MakeThemeCommand

public function handle(Filesystem $filesystem): int
{
$this->filesystem = $filesystem;

$module = $this->getModule();

$this->call('vendor:publish', [
Expand Down
48 changes: 48 additions & 0 deletions src/Modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 12 additions & 6 deletions src/ModulesPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});

Expand Down
14 changes: 10 additions & 4 deletions src/ModulesServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
79 changes: 79 additions & 0 deletions tests/Unit/ModuleProviderResolutionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

use Coolsam\Modules\Facades\FilamentModules;

test('can resolve provider class from file namespace declaration', function () {
$module = $this->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'
<?php

namespace MyCompany\CRM\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class CustomNamespaceServiceProvider extends ServiceProvider
{
public function register(): void
{
}
}
PHP);

expect(FilamentModules::resolveClassFromProviderFile($providerPath))
->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'
<?php

namespace MyCompany\CRM\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class BlogServiceProvider extends ServiceProvider
{
public function register(): void
{
}
}
PHP);

$namespace = FilamentModules::resolveProviderClass($providerPath);

expect(FilamentModules::findModuleNameForPath($providerPath))->toBe('CustomProviderModule');
expect(str($namespace)->afterLast('\\')->startsWith('Blog'))->toBeTrue();

require_once $providerPath;

$this->app->register($namespace);

expect(collect($this->app->getProviders($namespace)))->not->toBeEmpty();
});
20 changes: 10 additions & 10 deletions tests/Unit/ModulesServiceProviderBootTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@
}

file_put_contents($providerPath, <<<'PHP'
<?php
<?php

namespace Modules\Blog\Providers;
namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\ServiceProvider;

class BlogServiceProvider extends ServiceProvider
{
public function register(): void
{
}
}
PHP);
class BlogServiceProvider extends ServiceProvider
{
public function register(): void
{
}
}
PHP);

require_once $providerPath;

Expand Down
Loading