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
{