Modules previously had their Filament resources injected into the main
admin panel and surfaced legacy addAdminLink() items as sidebar nav.
Invert this: each module owns a dedicated Filament panel and a navbar
panel switcher moves between the main panel and module panels.
- Add App\Contracts\Modules\PanelProvider: an abstract base that
pre-configures id (= module key), path (admin/{key}), middleware,
auth, branding, the shared admin theme/sidebar (viteTheme +
sidebarWidth 14.5rem for visual consistency), a Dashboard page, the
shared plugins, and discovery of the module's own
Filament/{Resources,Pages,Widgets}. Modules register it via their
providers list and override only moduleKey().
- Add PanelSwitcherPlugin: a topbar-left dropdown listing the main
panel plus every module panel the user can access, on every panel.
- Stop injecting module components into the core admin/system panels:
remove FilamentPanelExtender, its beforeResolving('filament') hook,
probeFilament(), and the boot-cache filament field (schema bump).
- Remove the legacy link APIs (addAdminLink/addFrontendLink/
registerLinks) and their blade/view consumers.
- Gate module panels via access:{module-key} in canAccessPanel(),
with the view:modules fallback.
- Migrate the Sample module to ship its own panel provider and drop
its old admin controller/route.
206 lines
7.0 KiB
PHP
206 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Contracts\Modules;
|
|
|
|
use App\Enums\NavigationGroup;
|
|
use App\Filament\Plugins\ClearCachesPlugin;
|
|
use App\Filament\Plugins\LanguageSwitcherPlugin;
|
|
use App\Filament\Plugins\PanelSwitcherPlugin;
|
|
use App\Filament\Plugins\SidebarCollapseTogglePlugin;
|
|
use Filament\Http\Middleware\Authenticate;
|
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
|
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
|
use Filament\Navigation\NavigationItem;
|
|
use Filament\Pages\Dashboard;
|
|
use Filament\Panel;
|
|
use Filament\PanelProvider as FilamentPanelProvider;
|
|
use Filament\Support\Colors\Color;
|
|
use Filament\Support\Icons\Heroicon;
|
|
use Filament\View\PanelsRenderHook;
|
|
use Illuminate\Contracts\View\Factory;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
|
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
|
use Illuminate\Session\Middleware\StartSession;
|
|
use Illuminate\Support\Facades\Blade;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
|
use Override;
|
|
use ReflectionClass;
|
|
|
|
/**
|
|
* Batteries-included base Filament panel provider for modules.
|
|
*
|
|
* A module ships its own Filament panel by extending this class and supplying
|
|
* only {@see moduleKey()}; everything else — id, path, middleware, auth,
|
|
* branding, theme, navigation, the panel switcher, and convention-based
|
|
* discovery of the module's own Filament/{Resources,Pages,Widgets} — is
|
|
* pre-configured here.
|
|
*
|
|
* Convention: the extending provider lives at
|
|
* {module-root}/Providers/Filament/XxxAdminPanelProvider.php
|
|
* so the module root is three levels up from the provider file, and the root
|
|
* namespace is everything before `\Providers\`. Override moduleBasePath() /
|
|
* moduleRootNamespace() for non-standard layouts.
|
|
*
|
|
* The panel id equals the module key so per-module access (access:{module-key},
|
|
* resolved in User::canAccessPanel()) gates the panel.
|
|
*
|
|
* Octane-safe: no mutable instance state; all registration is idempotent.
|
|
*/
|
|
abstract class PanelProvider extends FilamentPanelProvider
|
|
{
|
|
/**
|
|
* The module's short machine key. Used as the panel id and path segment
|
|
* (path = `admin/{moduleKey}`) and as the access-permission suffix.
|
|
*/
|
|
abstract protected function moduleKey(): string;
|
|
|
|
#[Override]
|
|
public function panel(Panel $panel): Panel
|
|
{
|
|
$panel = $panel
|
|
->id($this->moduleKey())
|
|
->path('admin/'.$this->moduleKey())
|
|
->colors($this->colors())
|
|
->middleware([
|
|
EncryptCookies::class,
|
|
AddQueuedCookiesToResponse::class,
|
|
StartSession::class,
|
|
AuthenticateSession::class,
|
|
ShareErrorsFromSession::class,
|
|
PreventRequestForgery::class,
|
|
SubstituteBindings::class,
|
|
DisableBladeIconComponents::class,
|
|
DispatchServingFilamentEvent::class,
|
|
])
|
|
->authMiddleware([
|
|
Authenticate::class,
|
|
])
|
|
->sidebarCollapsibleOnDesktop()
|
|
->sidebarWidth('14.5rem')
|
|
->navigationGroups([
|
|
NavigationGroup::Operations->name,
|
|
NavigationGroup::Config->name,
|
|
NavigationGroup::Developers->name,
|
|
])
|
|
->navigationItems([
|
|
// Labels should be in a closure to allow for translation
|
|
NavigationItem::make()
|
|
->label(fn (): string => __('common.go_back_to', ['name' => config('app.name')]))
|
|
->icon(Heroicon::OutlinedArrowUturnLeft)
|
|
->url('/'),
|
|
])
|
|
->pages([
|
|
Dashboard::class,
|
|
])
|
|
->plugins([
|
|
PanelSwitcherPlugin::make(),
|
|
ClearCachesPlugin::make(),
|
|
LanguageSwitcherPlugin::make(),
|
|
SidebarCollapseTogglePlugin::make(),
|
|
])
|
|
->bootUsing(function (): void {
|
|
activity()->enableLogging();
|
|
})
|
|
->brandName('phpvms')
|
|
->brandLogo(fn (): Factory|View => view('filament.shared.brand'))
|
|
->brandLogoHeight('3rem')
|
|
->font('Geist')
|
|
->favicon(asset('assets/img/favicon.png'))
|
|
->renderHook(
|
|
PanelsRenderHook::HEAD_END,
|
|
fn (): string => Blade::render("@vite('resources/js/admin/app.js')"),
|
|
)
|
|
->viteTheme('resources/css/filament/admin/theme.css')
|
|
->unsavedChangesAlerts()
|
|
->spa(hasPrefetching: config('phpvms.use_prefetching_in_admin', false))
|
|
->breadcrumbs(false)
|
|
->databaseNotifications()
|
|
->errorNotifications();
|
|
|
|
$this->discoverModuleComponents($panel);
|
|
|
|
return $panel;
|
|
}
|
|
|
|
/**
|
|
* The panel colour palette. Override to rebrand the module panel.
|
|
*
|
|
* @return array<string, array<int, string>|string>
|
|
*/
|
|
protected function colors(): array
|
|
{
|
|
return [
|
|
'primary' => Color::generatePalette('#067ec1'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Discover the module's own Filament components and register them on the
|
|
* panel. Only directories that exist are registered.
|
|
*/
|
|
protected function discoverModuleComponents(Panel $panel): void
|
|
{
|
|
$base = $this->moduleBasePath();
|
|
$namespace = rtrim($this->moduleRootNamespace(), '\\');
|
|
|
|
if ($namespace === '') {
|
|
return;
|
|
}
|
|
|
|
$filamentBase = $base.'/Filament';
|
|
|
|
$components = [
|
|
'Resources' => 'discoverResources',
|
|
'Pages' => 'discoverPages',
|
|
'Widgets' => 'discoverWidgets',
|
|
];
|
|
|
|
foreach ($components as $component => $method) {
|
|
$dir = $filamentBase.'/'.$component;
|
|
|
|
if (!is_dir($dir)) {
|
|
continue;
|
|
}
|
|
|
|
$panel->{$method}(
|
|
in: $dir,
|
|
for: $namespace.'\\Filament\\'.$component,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the module root directory.
|
|
*
|
|
* Default: 3 levels up from the provider file, which lives at
|
|
* `{module-root}/Providers/Filament/XxxAdminPanelProvider.php`.
|
|
*/
|
|
protected function moduleBasePath(): string
|
|
{
|
|
return dirname((string) new ReflectionClass(static::class)->getFileName(), 3);
|
|
}
|
|
|
|
/**
|
|
* Resolve the root PHP namespace of this module.
|
|
*
|
|
* Default: everything before `\Providers\` in the provider's FQCN, e.g.
|
|
* `Modules\Sample\Providers\Filament\SampleAdminPanelProvider` → `Modules\Sample`.
|
|
* Returns '' when it can't be inferred; discovery is then skipped.
|
|
*/
|
|
protected function moduleRootNamespace(): string
|
|
{
|
|
if (!str_contains(static::class, '\\Providers\\')) {
|
|
return '';
|
|
}
|
|
|
|
return Str::beforeLast(static::class, '\\Providers\\');
|
|
}
|
|
}
|