From ebae1e9fb6007989db8adf7e6bfd9ddf95c47d56 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 08:26:33 -0400 Subject: [PATCH] Adopt shared route aliases; scope Livewire panel and Filament slug The bare env-kit.* route names now resolve through package-tools' BareRouteNameAliases instead of a hand-rolled resolver; behaviour is unchanged, and the deprecated env-kit limiter keeps its warn-once delegate. The Livewire panel becomes laranail-env-kit-webui.panel and the Filament slug laranail-env-kit-webui; the old component name stays registered and {panel}/env-kit redirects under its old route name, each announcing its replacement once. Views and translations also answer to laranail/env-kit-webui; translation calls stay on the hyphen form so host overrides keep applying. --- CHANGELOG.md | 28 +++ README.md | 6 + composer.json | 2 +- docs/security.md | 5 +- docs/themes.md | 30 ++- phpstan.neon.dist | 5 +- .../views/filament/env-kit-page.blade.php | 2 +- src/Adapters/AbstractThemeAdapter.php | 2 +- src/Adapters/Filament/EnvKitPage.php | 39 +++- src/Adapters/Filament/RedirectLegacySlug.php | 47 +++++ src/Livewire/EnvKitPanelComponent.php | 2 +- src/Providers/EnvKitWebUIServiceProvider.php | 92 ++++---- src/Support/DeprecationNotices.php | 53 +++++ src/Support/RegisteredNames.php | 37 +++- tests/Feature/NamingConventionTest.php | 196 +++++++++++++----- tests/TestCase.php | 12 ++ 16 files changed, 435 insertions(+), 123 deletions(-) create mode 100644 src/Adapters/Filament/RedirectLegacySlug.php create mode 100644 src/Support/DeprecationNotices.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c44093d..619e6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `tests/Feature/NamingConventionTest.php`, which reads the live router, `RateLimiter`, Artisan kernel and middleware-alias map and fails on any bare route name, limiter, command or alias this package owns. +- `Support\DeprecationNotices`, which announces a deprecated Livewire name or Filament slug once + per process with `E_USER_DEPRECATED`, and `Adapters\Filament\RedirectLegacySlug`, which answers + the page's old URL. +- `RegisteredNames` constants for the view and translation namespaces, the Livewire panel name and + the Filament slug, with their deprecated predecessors. ### Changed @@ -22,6 +27,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 namespace, so the bare names were a collision waiting to happen. - **The API rate limiter is vendor-scoped.** The routes now use `throttle:laranail-env-kit-webui.api` instead of `throttle:env-kit`. The limit and its bucket key are unchanged. +- The deprecated bare route names are served by `laranail/package-tools`' shared + `BareRouteNameAliases` (declared with `$package->hasDeprecatedRouteNames()`), replacing the + provider's own resolver. Behaviour is unchanged: one logged warning per name, chained to any + resolver registered earlier, never shadowing a host route. The warning now reads "the route name + [...] is deprecated and will stop resolving no earlier than the next minor after 0.1; use + [...]". Requires `laranail/package-tools ^0.1.3`. +- **The Livewire panel is vendor-scoped:** `laranail-env-kit-webui.panel`, which the Filament page + now embeds. +- **The Filament page slug is vendor-scoped:** `laranail-env-kit-webui`, so the page is served at + `{panel}/laranail-env-kit-webui` under the route name + `filament.{panel}.pages.laranail-env-kit-webui`. +- Views and translations are registered under `laranail/env-kit-webui::` as well as + `laranail-env-kit-webui::`, over the same paths. The package's own view calls use the slash + form; its translation calls stay on the hyphen form, which is where a host's overrides in + `lang/vendor/laranail-env-kit-webui/` are read from. +- `NamingConventionTest` runs on package-tools' `AssertsRegisteredNames`, and also covers the + Livewire component, the view and translation namespaces and the Filament slug. ### Deprecated @@ -31,6 +53,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the scoped names there. Earliest removal: the next minor after 0.1. - The bare `env-kit` rate limiter. It is still registered, delegates to the same limit, and logs a warning naming `laranail-env-kit-webui.api`. Earliest removal: the next minor after 0.1. +- The Livewire name `env-kit-panel`. It is still registered for the same component; mounting it + raises one `E_USER_DEPRECATED` naming `laranail-env-kit-webui.panel`. Earliest removal: the + next minor after 0.1. +- The Filament page slug `env-kit`. `{panel}/env-kit` redirects to the page, keeping the query + string, under the page's old route name `filament.{panel}.pages.env-kit`, and raises one + `E_USER_DEPRECATED`. Earliest removal: the next minor after 0.1. ### Fixed diff --git a/README.md b/README.md index 7ff669d..b8d617a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,12 @@ curl -X PUT -H "Authorization: Bearer $SANCTUM_TOKEN" -H "Accept: application/js https://acme.test/api/v1/env-kit/keys/MAIL_HOST ``` +Route names are `laranail-env-kit-webui.keys.*` and `laranail-env-kit-webui.panel`, the API +limiter is `laranail-env-kit-webui.api`, the Livewire panel is `laranail-env-kit-webui.panel`, +and the Filament page slug is `laranail-env-kit-webui`. The older bare names (`env-kit.*` +routes, the `env-kit` limiter, the `env-kit-panel` component and the `env-kit` page slug) are +deprecated aliases that still work, each announcing its replacement once. + The full walkthrough is in [JSON API](docs/api.md); everything else is in the [documentation index](#documentation). ## Documentation diff --git a/composer.json b/composer.json index 8730728..8deb4fb 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "require": { "php": "^8.4.1 || ^8.5", "laranail/env-kit": "^0.1", - "laranail/package-tools": "^0.1", + "laranail/package-tools": "^0.1.3", "illuminate/contracts": "^13.0", "illuminate/http": "^13.0", "illuminate/routing": "^13.0", diff --git a/docs/security.md b/docs/security.md index da96c22..0a47caa 100644 --- a/docs/security.md +++ b/docs/security.md @@ -15,8 +15,9 @@ While disabled, every API and panel route returns `404` — checked per request, the surface can be toggled at runtime. The **in-app surfaces honour the same switch**: the Filament page's `canAccess()` and -the Livewire `env-kit-panel` component both refuse (Filament hides it / the component -403s) unless `enabled` is true — so embedding them anywhere can't bypass the gate. +the Livewire `laranail-env-kit-webui.panel` component (also reachable through its deprecated +`env-kit-panel` alias) both refuse (Filament hides it / the component 403s) unless `enabled` +is true — so embedding them anywhere can't bypass the gate. ## 2. Auth-gated diff --git a/docs/themes.md b/docs/themes.md index 91415dc..f5c7b6d 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -30,10 +30,16 @@ root class; every built-in theme ships `dark:` variants: ## How it works -All themes render one Blade view (`laranail-env-kit-webui::panel`), parameterised by a CSS +All themes render one Blade view (`laranail/env-kit-webui::panel`), parameterised by a CSS class map — there is no per-theme view duplication. The view is fed an `EnvKitViewModel` built from the engine (keys/values with secrets masked). +Views answer to both `laranail/env-kit-webui::` (canonical) and `laranail-env-kit-webui::`, over +the same paths, so an override in `resources/views/vendor/laranail-env-kit-webui/` applies to +either. Translations are registered under both forms too. The package's own views translate +through `laranail-env-kit-webui::`, so overrides keep living in +`lang/vendor/laranail-env-kit-webui/`. + ## Custom themes Extend `AbstractThemeAdapter` (supply `name()` + a `classes()` map) and register it @@ -77,8 +83,26 @@ public function panel(Panel $panel): Panel } ``` -The page embeds the reactive Livewire panel. (Requires `livewire/livewire`, which -Filament already depends on.) +The page is served at `{panel}/laranail-env-kit-webui` (route name +`filament.{panel}.pages.laranail-env-kit-webui`) and embeds the reactive Livewire panel, +`laranail-env-kit-webui.panel`. (Requires `livewire/livewire`, which Filament already depends +on.) To embed the panel elsewhere: + +```blade +@livewire('laranail-env-kit-webui.panel') +``` + +### Deprecated names + +These still work and are removed no earlier than the next minor after 0.1: + +| Deprecated | Replacement | How it still works | +|---|---|---| +| Page slug `env-kit` | `laranail-env-kit-webui` | `{panel}/env-kit` redirects to the page, keeping the query string, under the old route name `filament.{panel}.pages.env-kit`. | +| Livewire `env-kit-panel` | `laranail-env-kit-webui.panel` | Still registered for the same component. | + +Each raises one `E_USER_DEPRECATED` per process naming the replacement, which Laravel writes to +the `deprecations` log channel when one is configured. ## Laravel Nova diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a0ad3a6..458e2c2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,8 +11,9 @@ parameters: # at runtime — only loaded when a consumer with Nova references it. - src/Adapters/Nova/EnvKitTool.php ignoreErrors: - # The `laranail-env-kit-webui::` view namespace is registered at RUNTIME by the service - # provider (package-tools' bootPackageViews()), and PHPStan does not execute that — so + # The `laranail/env-kit-webui::` and `laranail-env-kit-webui::` view namespaces are + # registered at RUNTIME by the service provider (loadViewsFrom() plus package-tools' + # NamespaceForms::mirror()), and PHPStan does not execute that — so # larastan cannot resolve the literal to a file and reports it as a plain string rather # than a view-string. Both views do exist: resources/views/panel.blade.php and # resources/views/livewire/env-kit-panel.blade.php. diff --git a/resources/views/filament/env-kit-page.blade.php b/resources/views/filament/env-kit-page.blade.php index e3318d2..7f773f7 100644 --- a/resources/views/filament/env-kit-page.blade.php +++ b/resources/views/filament/env-kit-page.blade.php @@ -1,3 +1,3 @@ - @livewire('env-kit-panel') + @livewire('laranail-env-kit-webui.panel') diff --git a/src/Adapters/AbstractThemeAdapter.php b/src/Adapters/AbstractThemeAdapter.php index 21f05b0..6cf22ad 100644 --- a/src/Adapters/AbstractThemeAdapter.php +++ b/src/Adapters/AbstractThemeAdapter.php @@ -26,7 +26,7 @@ public function render(EnvKitViewModel $viewModel): View $classes['root'] = trim(($classes['root'] ?? '') . ' dark'); } - return app(ViewFactory::class)->make('laranail-env-kit-webui::panel', [ + return app(ViewFactory::class)->make('laranail/env-kit-webui::panel', [ 'vm' => $viewModel, 'theme' => $classes, ]); diff --git a/src/Adapters/Filament/EnvKitPage.php b/src/Adapters/Filament/EnvKitPage.php index 9cb377b..42d23b1 100644 --- a/src/Adapters/Filament/EnvKitPage.php +++ b/src/Adapters/Filament/EnvKitPage.php @@ -4,14 +4,22 @@ namespace Simtabi\Laranail\EnvKit\WebUI\Adapters\Filament; +use Filament\Panel; use Filament\Pages\Page; +use Filament\Pages\PageConfiguration; +use Illuminate\Support\Facades\Route; use Simtabi\Laranail\EnvKit\WebUI\Support\PanelAccess; +use Simtabi\Laranail\EnvKit\WebUI\Support\RegisteredNames; /** * A Filament panel page that surfaces the EnvKit editor (the reactive Livewire * panel) inside a Filament admin panel. Register it via {@see EnvKitPlugin} on * your panel. Only autoloaded when Filament is installed (the consumer references * it from their panel config). + * + * The slug is vendor-scoped (`laranail-env-kit-webui`), since Filament keys page + * URLs and route names by it. The old `env-kit` slug still answers: see + * {@see self::registerRoutes()}. */ final class EnvKitPage extends Page { @@ -19,9 +27,36 @@ final class EnvKitPage extends Page protected static ?string $title = 'EnvKit'; - protected static ?string $slug = 'env-kit'; + protected static ?string $slug = RegisteredNames::FILAMENT_SLUG; - protected string $view = 'laranail-env-kit-webui::filament.env-kit-page'; + protected string $view = 'laranail/env-kit-webui::filament.env-kit-page'; + + /** + * Register the page, plus the deprecated `env-kit` slug as a redirect to it. + * + * The redirect is registered in the same group as the page, so it gets the + * panel's prefix, tenancy and auth middleware, and takes the page's old + * route name (`filament..pages.env-kit`), so a host's + * `route('filament.admin.pages.env-kit')` still generates a working link. + * It is skipped for a configured or clustered page, whose URL never was the + * bare slug. + * + * @deprecated the redirect only; it goes with the old slug, no earlier than + * the next minor after 0.1. + */ + public static function registerRoutes(Panel $panel, ?PageConfiguration $configuration = null): void + { + parent::registerRoutes($panel, $configuration); + + if ($configuration !== null || filled(self::getCluster())) { + return; + } + + Route::name('pages.')->group(static function (): void { + Route::get('/' . RegisteredNames::LEGACY_FILAMENT_SLUG, RedirectLegacySlug::class) + ->name(RegisteredNames::LEGACY_FILAMENT_SLUG); + }); + } /** Honour the same disabled-by-default + gate policy as the HTTP surface. */ public static function canAccess(): bool diff --git a/src/Adapters/Filament/RedirectLegacySlug.php b/src/Adapters/Filament/RedirectLegacySlug.php new file mode 100644 index 0000000..1d3e9f4 --- /dev/null +++ b/src/Adapters/Filament/RedirectLegacySlug.php @@ -0,0 +1,47 @@ +getPathInfo(), '/'); + $legacy = '/' . RegisteredNames::LEGACY_FILAMENT_SLUG; + + if (str_ends_with($path, $legacy)) { + $path = substr($path, 0, -strlen($legacy)); + } + + $query = $request->getQueryString(); + + return new RedirectResponse( + $request->getSchemeAndHttpHost() . $request->getBaseUrl() . $path . '/' . RegisteredNames::FILAMENT_SLUG + . ($query !== null && $query !== '' ? '?' . $query : ''), + ); + } +} diff --git a/src/Livewire/EnvKitPanelComponent.php b/src/Livewire/EnvKitPanelComponent.php index 91972f7..ca98188 100644 --- a/src/Livewire/EnvKitPanelComponent.php +++ b/src/Livewire/EnvKitPanelComponent.php @@ -68,7 +68,7 @@ public function save(): void public function render(): View { - return app(ViewFactory::class)->make('laranail-env-kit-webui::livewire.env-kit-panel'); + return app(ViewFactory::class)->make('laranail/env-kit-webui::livewire.env-kit-panel'); } private function refreshVariables(): void diff --git a/src/Providers/EnvKitWebUIServiceProvider.php b/src/Providers/EnvKitWebUIServiceProvider.php index 122542c..e70ba9c 100644 --- a/src/Providers/EnvKitWebUIServiceProvider.php +++ b/src/Providers/EnvKitWebUIServiceProvider.php @@ -5,11 +5,10 @@ namespace Simtabi\Laranail\EnvKit\WebUI\Providers; use Livewire\Livewire; +use Livewire\Component; use Illuminate\Http\Request; use Composer\InstalledVersions; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\URL; -use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Facades\Route; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Contracts\Config\Repository; @@ -18,6 +17,9 @@ use Simtabi\Laranail\EnvKit\WebUI\Doctor\Checks; use Simtabi\Laranail\EnvKit\WebUI\Extension\ThemeManager; use Simtabi\Laranail\EnvKit\WebUI\Support\RegisteredNames; +use Simtabi\Laranail\Package\Tools\Support\NamespaceForms; +use Simtabi\Laranail\Package\Tools\Enums\DeprecationNotice; +use Simtabi\Laranail\EnvKit\WebUI\Support\DeprecationNotices; use Simtabi\Laranail\EnvKit\WebUI\Livewire\EnvKitPanelComponent; use Simtabi\Laranail\Package\Tools\Providers\PackageServiceProvider; use Simtabi\Laranail\EnvKit\WebUI\Http\Middleware\EnvKitSecurityHeaders; @@ -30,8 +32,9 @@ final class EnvKitWebUIServiceProvider extends PackageServiceProvider /** * Deprecated names already warned about. Held for the life of the booted * application -- one request under PHP-FPM, one worker under Octane or a - * queue worker -- so a page with N links to a bare route name logs one - * line, not N. + * queue worker -- so the bare limiter logs one line, not one per throttled + * request. (The bare route names are announced once per process by + * package-tools' BareRouteNameAliases.) * * @var array */ @@ -47,7 +50,19 @@ public function configurePackage(Package $package): void ->field('Version', fn (): string => (string) InstalledVersions::getPrettyVersion('laranail/env-kit-webui')) ->field('Enabled', fn (): bool => (bool) config('laranail.env-kit-webui.enabled', false)), ) - ->hasDoctorChecks(Checks::all()); + ->hasDoctorChecks(Checks::all()) + // Keep the bare `env-kit.*` route names this package used to register + // resolving through route(), as deprecated aliases of the scoped names, + // with one logged warning per name. Consulted only for a name the router + // does not hold, so a host route genuinely named `env-kit.*` still wins, + // and chained to any resolver registered before it. `Route::has()` does + // not consult it: ask for the scoped name, or + // `$package->deprecatedRouteNames()->has()`. Earliest removal of the + // bare names: the next minor after 0.1. + ->hasDeprecatedRouteNames( + prefixes: [RegisteredNames::LEGACY_ROUTE_PREFIX => RegisteredNames::ROUTE_PREFIX], + notice: DeprecationNotice::Log, + ); } public function packageRegistered(): void @@ -58,11 +73,15 @@ public function packageRegistered(): void public function packageBooted(): void { - $this->loadViewsFrom($this->packagePath('resources/views'), 'laranail-env-kit-webui'); - $this->loadTranslationsFrom($this->packagePath('resources/lang'), 'laranail-env-kit-webui'); + $this->loadViewsFrom($this->packagePath('resources/views'), RegisteredNames::NAMESPACE_ALIAS); + $this->loadTranslationsFrom($this->packagePath('resources/lang'), RegisteredNames::NAMESPACE_ALIAS); + + // Add the canonical `laranail/env-kit-webui` form over the same paths, in + // both registries. The package's own views and classes use it; the hyphen + // form stays registered for hosts that already write it. + NamespaceForms::mirror($this->app, RegisteredNames::NAMESPACE); $this->registerThrottle(); - $this->resolveBareRouteNames(); $config = $this->app->make(Repository::class); // The lockdown guards are PREPENDED by the package (not in the overridable @@ -77,7 +96,18 @@ public function packageBooted(): void // The reactive panel is optional — registered only when Livewire is present. if (class_exists(Livewire::class)) { - Livewire::component('env-kit-panel', EnvKitPanelComponent::class); + // Scoped name first: Livewire maps a class back to the FIRST name it + // was registered under, so snapshots and Livewire::test() use it. + Livewire::component(RegisteredNames::LIVEWIRE_PANEL, EnvKitPanelComponent::class); + + // @deprecated The bare `env-kit-panel`, kept so `@livewire('env-kit-panel')` + // still renders; mounting it raises one E_USER_DEPRECATED. Earliest + // removal: the next minor after 0.1. + Livewire::component(RegisteredNames::LEGACY_LIVEWIRE_PANEL, EnvKitPanelComponent::class); + + Livewire::listen('mount', static function (Component $component): void { + DeprecationNotices::livewireMounted($component->getName()); + }); } } @@ -117,50 +147,6 @@ private function registerThrottle(): void }); } - /** - * Keep the bare `env-kit.*` route names this package used to register - * resolving through `route()`, as deprecated aliases of the scoped names. - * - * The hook is consulted only when a name is not found, so a host route - * genuinely named `env-kit.*` still wins. Laravel holds one resolver, so - * any resolver registered before this one is chained rather than replaced. - * `Route::has()` does not consult the hook — ask for the scoped name. - * - * @deprecated Since 0.1 for the bare names it serves. Earliest removal: - * the next minor after 0.1. - */ - private function resolveBareRouteNames(): void - { - $url = $this->app->make(UrlGenerator::class); - - /** @var callable|null $previous */ - $previous = (fn (): mixed => $this->missingNamedRouteResolver)->call($url); - - URL::resolveMissingNamedRoutesUsing( - function (string $name, mixed $parameters, ?bool $absolute) use ($previous): ?string { - $scoped = RegisteredNames::scopedRouteFor($name); - - if ($scoped !== null && Route::has($scoped)) { - $this->warnDeprecatedOnce('route:' . $name, sprintf( - '[laranail/env-kit-webui] The route name "%s" is deprecated; use "%s" instead.', - $name, - $scoped, - )); - - return URL::route($scoped, $parameters ?? [], $absolute ?? true); - } - - if (is_callable($previous)) { - $resolved = $previous($name, $parameters, $absolute); - - return is_string($resolved) ? $resolved : null; - } - - return null; - }, - ); - } - /** * Log a deprecation once per name for the life of the booted application. */ diff --git a/src/Support/DeprecationNotices.php b/src/Support/DeprecationNotices.php new file mode 100644 index 0000000..44c8abf --- /dev/null +++ b/src/Support/DeprecationNotices.php @@ -0,0 +1,53 @@ + */ + private static array $announced = []; + + public static function once(string $key, string $message): void + { + if (isset(self::$announced[$key])) { + return; + } + + self::$announced[$key] = true; + + trigger_error($message, E_USER_DEPRECATED); + } + + /** + * Raise the notice for a Livewire component mounted under the panel's + * deprecated bare name. Any other name is ignored. + */ + public static function livewireMounted(string $name): void + { + if ($name !== RegisteredNames::LEGACY_LIVEWIRE_PANEL) { + return; + } + + self::once('livewire:' . $name, sprintf( + 'laranail/env-kit-webui: the Livewire component name [%s] is deprecated and will be removed no earlier than the next minor after 0.1; use [%s].', + $name, + RegisteredNames::LIVEWIRE_PANEL, + )); + } + + /** + * Forget which names were announced. For test suites. + */ + public static function forget(): void + { + self::$announced = []; + } +} diff --git a/src/Support/RegisteredNames.php b/src/Support/RegisteredNames.php index 9f95033..98d61a9 100644 --- a/src/Support/RegisteredNames.php +++ b/src/Support/RegisteredNames.php @@ -6,7 +6,8 @@ /** * The names this package registers into framework-owned, flat registries - * (route names and rate limiters), and the deprecated bare names that still + * (route names, rate limiters, the Livewire component, the Filament page slug, + * view and translation namespaces), and the deprecated bare names that still * resolve to them. * * Every owned name carries the vendor and the package slug, so a sibling @@ -21,6 +22,40 @@ final class RegisteredNames /** The named rate limiter that throttles the JSON API. */ public const string LIMITER = 'laranail-env-kit-webui.api'; + /** The canonical view and translation namespace: the composer package name. */ + public const string NAMESPACE = 'laranail/env-kit-webui'; + + /** + * The hyphen view and translation namespace. Still registered over the same + * files, so a host writing `laranail-env-kit-webui::` keeps working; it is an + * alias, not deprecated. + */ + public const string NAMESPACE_ALIAS = 'laranail-env-kit-webui'; + + /** The Livewire name of the reactive panel. */ + public const string LIVEWIRE_PANEL = 'laranail-env-kit-webui.panel'; + + /** The Filament page slug, and so the last segment of its URL and route name. */ + public const string FILAMENT_SLUG = 'laranail-env-kit-webui'; + + /** + * The Livewire name the panel was registered under before it was vendor-scoped. + * + * @deprecated Since 0.1. Use LIVEWIRE_PANEL. Still registered for the same + * component; mounting it raises one E_USER_DEPRECATED. Earliest + * removal is the next minor after 0.1. + */ + public const string LEGACY_LIVEWIRE_PANEL = 'env-kit-panel'; + + /** + * The Filament page slug used before it was vendor-scoped. + * + * @deprecated Since 0.1. Use FILAMENT_SLUG. `{panel}/env-kit` still answers + * with a redirect to the page, under the old route name; earliest + * removal is the next minor after 0.1. + */ + public const string LEGACY_FILAMENT_SLUG = 'env-kit'; + /** * Prefix the routes were registered under before they were vendor-scoped. * diff --git a/tests/Feature/NamingConventionTest.php b/tests/Feature/NamingConventionTest.php index 2818312..b69febc 100644 --- a/tests/Feature/NamingConventionTest.php +++ b/tests/Feature/NamingConventionTest.php @@ -2,34 +2,51 @@ declare(strict_types=1); +use Filament\Panel; +use Livewire\Livewire; use Illuminate\Routing\Route; use Illuminate\Cache\RateLimiter; use Illuminate\Support\Facades\Log; -use Illuminate\Contracts\Console\Kernel; use Simtabi\Laranail\EnvKit\WebUI\Tests\TestCase; +use Illuminate\Support\Facades\Route as RouteFacade; +use Simtabi\Laranail\Package\Tools\Testing\NamingScope; use Simtabi\Laranail\EnvKit\WebUI\Support\RegisteredNames; +use Simtabi\Laranail\EnvKit\WebUI\Adapters\Filament\EnvKitPage; +use Simtabi\Laranail\EnvKit\WebUI\Livewire\EnvKitPanelComponent; +use Simtabi\Laranail\Package\Tools\Testing\AssertsRegisteredNames; -uses(TestCase::class); +uses(TestCase::class, AssertsRegisteredNames::class); /* * Every name this package registers into a framework-owned registry. * - * Route names, rate limiters, Artisan commands and middleware aliases are flat - * maps keyed by the name, so a second package claiming the same key does not - * collide loudly — it silently replaces the first. `env-kit.keys.index` and the - * `env-kit` limiter were exactly that kind of name, and `env-kit.*` is the - * sibling engine's (laranail/env-kit) own namespace besides. + * Route names, rate limiters, Artisan commands, middleware aliases, Livewire + * components and view and translation namespaces are flat maps keyed by the + * name, so a second package claiming the same key does not collide loudly -- it + * silently replaces the first. `env-kit.keys.index`, the `env-kit` limiter and + * the `env-kit-panel` component were exactly that kind of name, and `env-kit.*` + * is the sibling engine's (laranail/env-kit) own namespace besides. * - * These assertions read the live registries, not the provider source, so the - * guard holds whatever the registration code looks like. + * package-tools' AssertsRegisteredNames reads the live registries, not the + * provider source, so the guard holds whatever the registration code looks like. */ const OWN_PREFIX = 'laranail-env-kit-webui.'; +function envKitWebUiScope(): NamingScope +{ + // basePath is src/: package-tools 0.1.3 defaults it to the package root, which + // would also claim closures defined under vendor/ and tests/ as this package's. + return NamingScope::for( + 'laranail/env-kit-webui', + 'Simtabi\\Laranail\\EnvKit\\WebUI\\', + basePath: dirname(__DIR__, 2) . '/src', + ); +} + /** - * The routes this package registered — identified by the controller they - * dispatch to, not by their name, so a route registered under a bare name is - * still found and still fails. + * The routes this package registered, identified by the controller they + * dispatch to. * * @return list */ @@ -56,18 +73,8 @@ function registeredLimiterNames(): array } it('registers every one of its routes under laranail-env-kit-webui.*', function (): void { - $routes = envKitWebUiRoutes(); - - // Non-vacuity: a discovery that finds nothing would pass every check below. - expect($routes)->toHaveCount(6); - - $names = array_map(static fn (Route $route): ?string => $route->getName(), $routes); - - foreach ($names as $name) { - expect($name)->toBeString()->toStartWith(OWN_PREFIX); - } - - expect($names)->toEqualCanonicalizing([ + // Non-vacuity: the package ships six routes. + expect($this->assertRouteNamesScoped(envKitWebUiScope(), atLeast: 6))->toEqualCanonicalizing([ 'laranail-env-kit-webui.keys.index', 'laranail-env-kit-webui.keys.show', 'laranail-env-kit-webui.keys.store', @@ -98,50 +105,66 @@ function registeredLimiterNames(): array }); it('owns no bare limiter other than the deprecated env-kit alias', function (): void { - $ours = array_values(array_filter( - registeredLimiterNames(), - static fn (string $name): bool => str_contains($name, 'env-kit'), - )); + expect($this->assertRateLimitersScoped(envKitWebUiScope(), deprecated: [RegisteredNames::LEGACY_LIMITER], atLeast: 1)) + ->toBe([RegisteredNames::LIMITER]); +}); - // The deprecated alias is the one sanctioned bare name; anything else - // containing env-kit must carry the vendor prefix. - foreach (array_diff($ours, [RegisteredNames::LEGACY_LIMITER]) as $name) { - expect($name)->toStartWith(OWN_PREFIX); - } +it('registers no Artisan command and no middleware alias of its own', function (): void { + // atLeast: 0 -- the package registers neither today; a bare one it owns + // still fails, and the env:* / laranail::env-kit.* commands in the kernel + // belong to laranail/env-kit, the engine, not to this package. + expect($this->assertCommandNamesScoped(envKitWebUiScope(), atLeast: 0))->toBe([]) + ->and($this->assertMiddlewareAliasesScoped(envKitWebUiScope(), atLeast: 0))->toBe([]); +}); - expect($ours)->toContain(RegisteredNames::LIMITER); +it('registers its Livewire panel under laranail-env-kit-webui.panel, keeping env-kit-panel as a deprecated alias', function (): void { + expect($this->assertLivewireComponentsScoped( + envKitWebUiScope(), + deprecated: [RegisteredNames::LEGACY_LIVEWIRE_PANEL], + atLeast: 1, + ))->toBe([RegisteredNames::LIVEWIRE_PANEL]); + + // Livewire maps a class back to the FIRST name it was registered under. + expect(app('livewire.finder')->normalizeName(EnvKitPanelComponent::class))->toBe(RegisteredNames::LIVEWIRE_PANEL); }); -it('registers no Artisan command and no middleware alias of its own', function (): void { - expect(app(Kernel::class)->all())->not->toBeEmpty(); - // Asserted rather than assumed: the env:* / laranail::env-kit.* commands - // in the kernel belong to laranail/env-kit, the engine. Anything this - // package adds must take the laranail::env-kit-webui. shape. - $commands = array_keys(app(Kernel::class)->all()); - - foreach ($commands as $command) { - if (str_contains($command, 'webui')) { - expect($command)->toStartWith('laranail::env-kit-webui.'); - } - } +it('registers its views and translations under both namespace forms', function (): void { + $scope = envKitWebUiScope(); - foreach (array_keys(app('router')->getMiddleware()) as $alias) { - if (str_contains($alias, 'env-kit')) { - expect($alias)->toStartWith('laranail-env-kit-webui'); - } - } + expect($this->assertViewNamespacesScoped($scope, atLeast: 2)) + ->toContain(RegisteredNames::NAMESPACE, RegisteredNames::NAMESPACE_ALIAS) + ->and($this->assertTranslationNamespacesScoped($scope, atLeast: 2)) + ->toContain(RegisteredNames::NAMESPACE, RegisteredNames::NAMESPACE_ALIAS); + + // The canonical view form resolves through exactly the hint paths of the hyphen + // form, the application's published override directory included, so moving + // the package's own calls to it cannot bypass a host's override. + $hints = view()->getFinder()->getHints(); + + expect($hints[RegisteredNames::NAMESPACE])->toBe($hints[RegisteredNames::NAMESPACE_ALIAS]); + + expect(view()->exists('laranail-env-kit-webui::panel'))->toBeTrue() + ->and(view()->exists('laranail/env-kit-webui::panel'))->toBeTrue() + ->and(__('laranail-env-kit-webui::messages.key'))->toBe(__('laranail/env-kit-webui::messages.key')) + ->and(__('laranail/env-kit-webui::messages.key'))->not->toBe('laranail/env-kit-webui::messages.key'); }); it('still resolves the deprecated bare route names, and warns', function (): void { Log::spy(); + $this->assertDeprecatedRouteNamesResolve( + ['env-kit.panel' => 'laranail-env-kit-webui.panel', 'env-kit.keys.show' => 'laranail-env-kit-webui.keys.show'], + parameters: ['env-kit.keys.show' => ['key' => 'APP_NAME']], + ); + expect(route('env-kit.panel', [], false))->toBe('/env-kit') ->and(route('env-kit.keys.show', ['key' => 'APP_NAME'], false))->toBe('/api/v1/env-kit/keys/APP_NAME') - ->and(route('env-kit.keys.index'))->toBe(route('laranail-env-kit-webui.keys.index')); + ->and(route('env-kit.keys.index'))->toBe(route('laranail-env-kit-webui.keys.index')) + ->and(RouteFacade::has('env-kit.panel'))->toBeFalse(); Log::shouldHaveReceived('warning') - ->withArgs(static fn (string $message): bool => str_contains($message, 'env-kit.panel') - && str_contains($message, 'laranail-env-kit-webui.panel')) + ->withArgs(static fn (string $message): bool => str_contains($message, '[env-kit.panel]') + && str_contains($message, '[laranail-env-kit-webui.panel]')) ->once(); }); @@ -182,10 +205,10 @@ function registeredLimiterNames(): array } Log::shouldHaveReceived('warning') - ->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.panel"')) + ->withArgs(static fn (string $message): bool => str_contains($message, '[env-kit.panel]')) ->once(); Log::shouldHaveReceived('warning') - ->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.keys.index"')) + ->withArgs(static fn (string $message): bool => str_contains($message, '[env-kit.keys.index]')) ->once(); }); @@ -203,3 +226,64 @@ function registeredLimiterNames(): array ->withArgs(static fn (string $message): bool => str_contains($message, RegisteredNames::LEGACY_LIMITER)) ->once(); }); + +it('mounts the panel under its deprecated env-kit-panel name, announcing the replacement once', function (): void { + config(['laranail.env-kit-webui.enabled' => true]); + $this->bindEnv("APP_NAME=Acme\n"); + + $notices = []; + set_error_handler(function (int $level, string $message) use (&$notices): bool { + $notices[] = $message; + + return true; + }, E_USER_DEPRECATED); + + try { + Livewire::test(RegisteredNames::LIVEWIRE_PANEL)->assertSee('APP_NAME'); + expect($notices)->toBe([]); + + Livewire::test(RegisteredNames::LEGACY_LIVEWIRE_PANEL)->assertSee('APP_NAME'); + Livewire::test(RegisteredNames::LEGACY_LIVEWIRE_PANEL)->assertSee('APP_NAME'); + } finally { + restore_error_handler(); + } + + expect($notices)->toHaveCount(1) + ->and($notices[0])->toContain('[env-kit-panel] is deprecated') + ->and($notices[0])->toContain('[' . RegisteredNames::LIVEWIRE_PANEL . ']'); +}); + +it('serves the Filament page at the scoped slug and redirects the deprecated env-kit slug to it', function (): void { + // The page's route registration reads Filament's manager. + app()->register(Filament\FilamentServiceProvider::class); + + $panel = Panel::make()->id('envkit-test')->path('admin'); + + // The group Filament wraps every panel route in: name prefix and path. + RouteFacade::name('filament.envkit-test.')->prefix('admin')->group( + static fn () => EnvKitPage::registerRoutes($panel), + ); + app('router')->getRoutes()->refreshNameLookups(); + + expect(EnvKitPage::getSlug())->toBe(RegisteredNames::FILAMENT_SLUG) + ->and(RouteFacade::has('filament.envkit-test.pages.laranail-env-kit-webui'))->toBeTrue() + // The old route name still generates a link -- to the redirect. + ->and(route('filament.envkit-test.pages.env-kit', [], false))->toBe('/admin/env-kit'); + + $notices = []; + set_error_handler(function (int $level, string $message) use (&$notices): bool { + $notices[] = $message; + + return true; + }, E_USER_DEPRECATED); + + try { + $this->get('/admin/env-kit?tab=keys')->assertRedirect('http://localhost/admin/laranail-env-kit-webui?tab=keys'); + $this->get('/admin/env-kit')->assertRedirect('http://localhost/admin/laranail-env-kit-webui'); + } finally { + restore_error_handler(); + } + + expect($notices)->toHaveCount(1) + ->and($notices[0])->toContain('[env-kit] is deprecated'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index c25985c..db74453 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,12 +7,24 @@ use Livewire\LivewireServiceProvider; use Illuminate\Support\Facades\Facade; use Orchestra\Testbench\TestCase as Orchestra; +use Simtabi\Laranail\EnvKit\WebUI\Support\DeprecationNotices; use Simtabi\Laranail\EnvKit\Headless\Contracts\EnvKitInterface; use Simtabi\Laranail\EnvKit\Headless\Providers\EnvKitServiceProvider; use Simtabi\Laranail\EnvKit\WebUI\Providers\EnvKitWebUIServiceProvider; +use Simtabi\Laranail\Package\Tools\Support\Routing\BareRouteNameAliases; abstract class TestCase extends Orchestra { + protected function setUp(): void + { + parent::setUp(); + + // Deprecated names are announced once per PROCESS by design, so a test + // asserting a notice must not depend on whether an earlier test fired it. + BareRouteNameAliases::forgetWarnings(); + DeprecationNotices::forget(); + } + /** @return list */ protected function getPackageProviders($app): array {