refactor(addons): remove Module shim, legacy NWIDART bindings, and update controllers/tests
- Removed `Module` and `ModuleRepository` compatibility shims. - Replaced NWIDART `Module` facade usages with the new `AddonRegistry`. - Updated frontend controllers to use `AddonRegistry` for module handling. - Added new feature tests to cover `AddonRegistry` lifecycle and rebuild cases. - Updated bootstrapping logic and service bindings to drop NWIDART dependencies. # Conflicts: # app/Models/Flight.php # app/Support/Utils.php
This commit is contained in:
parent
282a485fef
commit
d0cf54c3de
125
app/Addons/AddonRegistry.php
Normal file
125
app/Addons/AddonRegistry.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Addons;
|
||||
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\BootCache;
|
||||
use App\Exceptions\AddonNotFoundException;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Lifecycle façade for addons. Owns reads (find/all/enabled), enable/disable,
|
||||
* delete, install, update, asset linking, and Octane refresh.
|
||||
*
|
||||
* Reads return Addon Eloquent models. "enabled" is DB intent; "loaded" is the
|
||||
* boot-cache reality (what actually got PSR-4/provider-registered this worker).
|
||||
*/
|
||||
class AddonRegistry
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BootCache $bootCache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Find an addon by display name; null when not found.
|
||||
*/
|
||||
public function find(string $name): ?Addon
|
||||
{
|
||||
return $this->all()->first(fn (Addon $addon): bool => $addon->getName() === $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an addon by display name or throw.
|
||||
*
|
||||
* @throws AddonNotFoundException
|
||||
*/
|
||||
public function findOrFail(string $name): Addon
|
||||
{
|
||||
return $this->find($name) ?? throw new AddonNotFoundException($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every addon row (enabled and disabled).
|
||||
*
|
||||
* @return Collection<int, Addon>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
return Addon::query()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled addons (DB intent).
|
||||
*
|
||||
* @return Collection<int, Addon>
|
||||
*/
|
||||
public function enabled(): Collection
|
||||
{
|
||||
return Addon::query()->where('enabled', true)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the addon's code is actually loaded this worker — i.e. present in
|
||||
* the enabled set of the boot cache, which is what the autoloader reads.
|
||||
*/
|
||||
public function isLoaded(string $name): bool
|
||||
{
|
||||
return $this->bootCache->enabled()
|
||||
->contains(fn ($entry): bool => ($entry->name ?? basename($entry->path)) === $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable an addon: flip the DB flag and regenerate the boot cache.
|
||||
* No-op when the addon is unknown.
|
||||
*/
|
||||
public function enable(string $name): void
|
||||
{
|
||||
$this->setEnabled($name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable an addon: flip the DB flag and regenerate the boot cache.
|
||||
* No-op when the addon is unknown.
|
||||
*/
|
||||
public function disable(string $name): void
|
||||
{
|
||||
$this->setEnabled($name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an addon's DB row and regenerate the boot cache.
|
||||
* Does NOT remove files on disk. No-op when the addon is unknown.
|
||||
*/
|
||||
public function delete(string $name): void
|
||||
{
|
||||
$addon = $this->find($name);
|
||||
|
||||
if (!$addon instanceof Addon) {
|
||||
return;
|
||||
}
|
||||
|
||||
$addon->delete();
|
||||
|
||||
app(AddonDiscoveryService::class)->rebuildCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the enabled flag and regenerate the boot cache.
|
||||
*/
|
||||
private function setEnabled(string $name, bool $enabled): void
|
||||
{
|
||||
$addon = $this->find($name);
|
||||
|
||||
if (!$addon instanceof Addon) {
|
||||
return;
|
||||
}
|
||||
|
||||
$addon->enabled = $enabled;
|
||||
$addon->save();
|
||||
|
||||
app(AddonDiscoveryService::class)->rebuildCache();
|
||||
}
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Addons\Compat;
|
||||
|
||||
use App\Addons\Models\AddonManifest;
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\ManifestParser;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Compatibility shim wrapping an Addon model to satisfy the duck-typed
|
||||
* call surface previously provided by \Nwidart\Modules\Module.
|
||||
*
|
||||
* Lazy-parses module.json once per instance for name/description.
|
||||
* Octane-safe: instances are request-scoped value objects — no static
|
||||
* or global accumulators.
|
||||
*/
|
||||
class Module
|
||||
{
|
||||
/** @var AddonManifest|false|null false = parse attempted but failed; null = not yet parsed */
|
||||
private AddonManifest|false|null $manifest = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly Addon $addon,
|
||||
private readonly ManifestParser $parser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The manifest name, falling back to basename(path).
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->resolveManifest()?->name ?? basename($this->addon->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercase module name.
|
||||
*/
|
||||
public function getLowerName(): string
|
||||
{
|
||||
return strtolower($this->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* StudlyCase module name (mirrors nwidart Module::getStudlyName()).
|
||||
*/
|
||||
public function getStudlyName(): string
|
||||
{
|
||||
return Str::studly($this->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute filesystem path to the addon directory.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->addon->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to a sub-path within the addon directory.
|
||||
*/
|
||||
public function getExtraPath(string $path): string
|
||||
{
|
||||
return $this->getPath().'/'.$path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest description; null when absent or blank.
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->resolveManifest()?->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the addon is enabled.
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->addon->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the addon's enabled state, persist to DB, and regenerate the boot cache.
|
||||
*/
|
||||
public function setActive(bool $active): void
|
||||
{
|
||||
$this->addon->enabled = $active;
|
||||
$this->addon->save();
|
||||
|
||||
app(AddonDiscoveryService::class)->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the addon.
|
||||
*/
|
||||
public function enable(): void
|
||||
{
|
||||
$this->setActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the addon's DB row and regenerate the boot cache.
|
||||
*
|
||||
* Does NOT delete files on disk (full lifecycle handled in Phase 5).
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
$this->addon->delete();
|
||||
|
||||
app(AddonDiscoveryService::class)->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic property access for $shim->name and $shim->description.
|
||||
*
|
||||
* Unknown properties return null intentionally for nwidart compatibility —
|
||||
* nwidart Module exposes many public properties and callers may access them
|
||||
* without checking; null is a safe no-op sentinel.
|
||||
*/
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return match ($key) {
|
||||
'name' => $this->getName(),
|
||||
'description' => $this->getDescription(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic isset for property checks.
|
||||
*/
|
||||
public function __isset(string $key): bool
|
||||
{
|
||||
return in_array($key, ['name', 'description'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-parse the manifest once per instance.
|
||||
*
|
||||
* Returns null when the manifest is missing or invalid.
|
||||
*/
|
||||
private function resolveManifest(): ?AddonManifest
|
||||
{
|
||||
if ($this->manifest === null) {
|
||||
$parsed = $this->parser->parse($this->addon->path);
|
||||
$this->manifest = $parsed instanceof AddonManifest ? $parsed : false;
|
||||
}
|
||||
|
||||
return $this->manifest instanceof AddonManifest ? $this->manifest : null;
|
||||
}
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Addons\Compat;
|
||||
|
||||
use App\Addons\Models\AddonBootCache;
|
||||
use App\Addons\Support\BootCache;
|
||||
use App\Addons\Support\ManifestParser;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Compatibility repository satisfying the duck-typed surface of the nwidart
|
||||
* Module facade (container key: 'modules').
|
||||
*
|
||||
* Backed by AddonRuntime + ManifestParser; stateless and Octane-safe.
|
||||
* Do NOT bind this to the 'modules' key while nwidart is still active —
|
||||
* the binding is deferred to a later cutover task (Phase 8).
|
||||
*/
|
||||
class ModuleRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BootCache $registry,
|
||||
private readonly ManifestParser $parser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Return all addon shims keyed by module name.
|
||||
*
|
||||
* @return Collection<string, Module>
|
||||
*/
|
||||
public function all(): Collection
|
||||
{
|
||||
return $this->registry->all()
|
||||
->mapWithKeys(function (AddonBootCache $runtime): array {
|
||||
$addon = Addon::fromBootCache($runtime);
|
||||
$shim = $this->resolveShim($addon);
|
||||
|
||||
return [$shim->getName() => $shim];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return enabled addon shims keyed by module name.
|
||||
*
|
||||
* NOTE: reads from DB + parses manifests (cold/admin path). A future
|
||||
* optimisation could build from AddonRuntime::enabled() (boot cache)
|
||||
* once ModuleShim can be constructed from a cache row, avoiding the
|
||||
* per-row manifest parse entirely.
|
||||
*
|
||||
* @return Collection<string, Module>
|
||||
*/
|
||||
public function allEnabled(): Collection
|
||||
{
|
||||
// Intentionally reads from DB (addons table), not the boot cache — the cache may
|
||||
// be absent during installer/migration flows where DB is the only source of truth.
|
||||
return $this->registry->enabled()
|
||||
->mapWithKeys(function (AddonBootCache $runtime): array {
|
||||
$addon = Addon::fromBootCache($runtime);
|
||||
$shim = $this->resolveShim($addon);
|
||||
|
||||
return [$shim->getName() => $shim];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a module shim by name; returns null when not found.
|
||||
*
|
||||
* Matches by manifest name (case-sensitive), falling back to basename(path).
|
||||
* Iteration order follows Eloquent default (no explicit ORDER BY).
|
||||
*/
|
||||
public function find(string $name): ?Module
|
||||
{
|
||||
/** @var AddonBootCache $runtime */
|
||||
foreach ($this->registry->all() as $runtime) {
|
||||
$addon = Addon::fromBootCache($runtime);
|
||||
$shim = $this->resolveShim($addon);
|
||||
|
||||
if ($shim->getName() === $name) {
|
||||
return $shim;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a module is enabled by name.
|
||||
*/
|
||||
public function isEnabled(string $name): bool
|
||||
{
|
||||
return $this->find($name)?->isEnabled() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return config values for the nwidart module configuration surface.
|
||||
*
|
||||
* Supports: 'namespace' → 'Modules'; all other keys return $default.
|
||||
*/
|
||||
public function config(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($key === 'namespace') {
|
||||
return 'Modules';
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ModuleShim for the given Addon row.
|
||||
*/
|
||||
private function resolveShim(Addon $addon): Module
|
||||
{
|
||||
return new Module($addon, $this->parser);
|
||||
}
|
||||
}
|
||||
@ -147,6 +147,37 @@ class AddonDiscoveryService
|
||||
return $newAddons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the boot cache from current DB enabled state.
|
||||
*
|
||||
* Scans manifests on disk, matches each to its DB row, and writes only the
|
||||
* enabled addons (enabled-only cache invariant, D-13). This is the cache
|
||||
* regeneration that lifecycle mutations (enable/disable/delete/install/update)
|
||||
* must call — run() does NOT rewrite the cache on an installed system.
|
||||
*/
|
||||
public function rebuildCache(): void
|
||||
{
|
||||
$manifests = $this->scanLocation(config('addons.paths.base'));
|
||||
|
||||
/** @var Collection<int, Addon> $installed */
|
||||
$installed = Addon::where('enabled', true)->get();
|
||||
|
||||
/** @var list<AddonBootCache> $cacheRows */
|
||||
$cacheRows = [];
|
||||
|
||||
foreach ($manifests as $m) {
|
||||
$addon = $installed->first(fn (Addon $a): bool => ($a->registry_id === $m->registryId && $m->registryId !== null)
|
||||
|| $a->name === $m->name
|
||||
|| $a->namespace === $m->namespace);
|
||||
|
||||
if ($addon !== null) {
|
||||
$cacheRows[] = $this->buildBootCacheRow($m, true);
|
||||
}
|
||||
}
|
||||
|
||||
$this->bootCache->write($cacheRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate immediate subdirectories of $dir and parse each manifest.
|
||||
*
|
||||
|
||||
@ -2,17 +2,18 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Console\Concerns\PromptsForMissingInput;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput as PromptsForMissingInputContract;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Nwidart\Modules\Module;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand(name: 'module:setup-filament', description: 'Add Filament Support to a Module')]
|
||||
#[Signature('module:setup-filament {module : The name of the module}')]
|
||||
#[AsCommand(name: 'addon:setup-filament', description: 'Add Filament Support to a Module')]
|
||||
#[Signature('addon:setup-filament {module : The name of the module}')]
|
||||
class ModuleSetupFilament extends Command implements PromptsForMissingInputContract
|
||||
{
|
||||
use PromptsForMissingInput;
|
||||
@ -23,6 +24,12 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
|
||||
protected string $panelStub = 'resources/stubs/modules/admin-panel-provider.stub';
|
||||
|
||||
public function __construct(
|
||||
protected readonly AddonRegistry $addonRegistry,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
@ -30,18 +37,16 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
{
|
||||
$moduleName = $this->argument('module');
|
||||
|
||||
/** @var Module|null $module */
|
||||
$module = app('modules')->find($moduleName);
|
||||
|
||||
if (!$module) {
|
||||
$addon = $this->addonRegistry->find($moduleName);
|
||||
if (!$addon) {
|
||||
$this->components->error(sprintf("Module [%s] not found. Are you sure it's installed and enabled?", $moduleName));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->components->info('Setting up Filament for module: '.$module->getName());
|
||||
$this->components->info('Setting up Filament for module: '.$addon->getName());
|
||||
|
||||
$providerPath = str($module->getExtraPath(sprintf('%s/%s', $this->basePath, $this->className)))
|
||||
$providerPath = str($addon->getExtraPath(sprintf('%s/%s', $this->basePath, $this->className)))
|
||||
->replace('\\', '/')
|
||||
->append('.php')
|
||||
->toString();
|
||||
@ -49,7 +54,7 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
$namespace = Str::of($this->basePath)
|
||||
->replace('/', '\\')
|
||||
->prepend('\\')
|
||||
->prepend($this->getModuleNamespace($module))
|
||||
->prepend($addon->namespace)
|
||||
->toString();
|
||||
|
||||
$providerClass = sprintf('%s\%s', $namespace, $this->className);
|
||||
@ -57,8 +62,8 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
// Step 1: Scaffold the Provider
|
||||
$stubSuccess = false;
|
||||
|
||||
$this->components->task('Creating Admin Panel Provider', function () use ($module, $providerPath, &$stubSuccess): bool {
|
||||
$stubSuccess = $this->copyPanelStubToApp($module, $providerPath);
|
||||
$this->components->task('Creating Admin Panel Provider', function () use ($addon, $providerPath, &$stubSuccess): bool {
|
||||
$stubSuccess = $this->copyPanelStubToApp($addon, $providerPath);
|
||||
|
||||
// Return the boolean so the task component shows a Green Check or Red X
|
||||
return $stubSuccess;
|
||||
@ -69,20 +74,20 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
}
|
||||
|
||||
// Step 2: Register in module.json
|
||||
$this->components->task('Registering provider in module.json', function () use ($module, $providerClass): void {
|
||||
$this->components->task('Registering provider in module.json', function () use ($addon, $providerClass): void {
|
||||
$this->updateJsonArray(
|
||||
module_path($module->getName(), 'module.json'),
|
||||
$addon->getExtraPath('module.json'),
|
||||
'providers',
|
||||
$providerClass
|
||||
);
|
||||
});
|
||||
|
||||
// Step 3: Register in composer.json
|
||||
$this->components->task('Registering provider in composer.json', function () use ($module, $providerClass): void {
|
||||
$this->updateComposerJson($module, $providerClass);
|
||||
$this->components->task('Registering provider in composer.json', function () use ($addon, $providerClass): void {
|
||||
$this->updateComposerJson($addon, $providerClass);
|
||||
});
|
||||
|
||||
$this->components->info(sprintf('Module [%s] is now ready for Filament!', $module->getName()));
|
||||
$this->components->info(sprintf('Module [%s] is now ready for Filament!', $addon->getName()));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
@ -90,7 +95,7 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
/**
|
||||
* Copy the stub file and replace placeholders.
|
||||
*/
|
||||
protected function copyPanelStubToApp(Module $module, string $targetPath): bool
|
||||
protected function copyPanelStubToApp(Addon $addon, string $targetPath): bool
|
||||
{
|
||||
$panelStubPath = base_path($this->panelStub);
|
||||
|
||||
@ -103,9 +108,9 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
$stub = str(File::get($panelStubPath));
|
||||
|
||||
$replacements = [
|
||||
'STUDLY_NAME' => $module->getStudlyName(),
|
||||
'LOWER_NAME' => $module->getLowerName(),
|
||||
'MODULE_NAMESPACE' => app('modules')->config('namespace'),
|
||||
'STUDLY_NAME' => $addon->getStudlyName(),
|
||||
'LOWER_NAME' => $addon->getLowerName(),
|
||||
'MODULE_NAMESPACE' => config('addons.namespace'),
|
||||
];
|
||||
|
||||
foreach ($replacements as $key => $replacement) {
|
||||
@ -141,9 +146,9 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
/**
|
||||
* Helper to safely append the provider to the composer.json extra.laravel.providers array.
|
||||
*/
|
||||
protected function updateComposerJson(Module $module, string $providerClass): void
|
||||
protected function updateComposerJson(Addon $addon, string $providerClass): void
|
||||
{
|
||||
$path = module_path($module->getName(), 'composer.json');
|
||||
$path = $addon->getExtraPath('composer.json');
|
||||
|
||||
if (!File::exists($path)) {
|
||||
return;
|
||||
@ -160,12 +165,4 @@ class ModuleSetupFilament extends Command implements PromptsForMissingInputContr
|
||||
File::put($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root namespace for the module.
|
||||
*/
|
||||
protected function getModuleNamespace(Module $module): string
|
||||
{
|
||||
return app('modules')->config('namespace').'\\'.$module->getName();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,34 +2,37 @@
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Enums\NavigationGroup;
|
||||
use App\Services\ModuleService;
|
||||
use BackedEnum;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\EmbeddedTable;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Override;
|
||||
use UnitEnum;
|
||||
|
||||
class Addons extends Page implements Tables\Contracts\HasTable
|
||||
class Addons extends Page implements HasTable
|
||||
{
|
||||
use HasPageShield;
|
||||
use Tables\Concerns\InteractsWithTable;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = NavigationGroup::Developers;
|
||||
protected static string|UnitEnum|null $navigationGroup = NavigationGroup::Developers;
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = Heroicon::OutlinedPuzzlePiece;
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPuzzlePiece;
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return Str::of(__('common.addons'))->plural();
|
||||
@ -56,7 +59,7 @@ class Addons extends Page implements Tables\Contracts\HasTable
|
||||
->icon(Heroicon::OutlinedCheckCircle)
|
||||
->visible(fn (array $record): bool => !$record['enabled'])
|
||||
->action(function (array $record): void {
|
||||
app(ModuleService::class)->updateModule($record['name'], true);
|
||||
app(AddonRegistry::class)->enable($record['name']);
|
||||
$this->redirectRoute('filament.admin.pages.addons');
|
||||
}),
|
||||
|
||||
@ -66,7 +69,7 @@ class Addons extends Page implements Tables\Contracts\HasTable
|
||||
->icon(Heroicon::OutlinedMinusCircle)
|
||||
->visible(fn (array $record): bool => $record['enabled'])
|
||||
->action(function (array $record): void {
|
||||
app(ModuleService::class)->updateModule($record['name'], false);
|
||||
app(AddonRegistry::class)->disable($record['name']);
|
||||
$this->redirectRoute('filament.admin.pages.addons');
|
||||
}),
|
||||
|
||||
@ -77,7 +80,7 @@ class Addons extends Page implements Tables\Contracts\HasTable
|
||||
->visible(fn (array $record): bool => !$record['enabled'])
|
||||
->requiresConfirmation()
|
||||
->action(function (array $record): void {
|
||||
app(ModuleService::class)->deleteModule($record['name']);
|
||||
app(AddonRegistry::class)->delete($record['name']);
|
||||
$this->redirectRoute('filament.admin.pages.addons');
|
||||
}),
|
||||
])
|
||||
@ -102,7 +105,7 @@ class Addons extends Page implements Tables\Contracts\HasTable
|
||||
);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
public function content(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
@ -113,15 +116,9 @@ class Addons extends Page implements Tables\Contracts\HasTable
|
||||
|
||||
public function getModulesRecords(): Collection
|
||||
{
|
||||
$modulesStatuses = [];
|
||||
|
||||
foreach (Module::all() as $module) {
|
||||
$modulesStatuses[] = [
|
||||
'name' => $module->getName(),
|
||||
'enabled' => $module->isEnabled(),
|
||||
];
|
||||
}
|
||||
|
||||
return collect($modulesStatuses);
|
||||
return app(AddonRegistry::class)->all()->map(fn ($addon): array => [
|
||||
'name' => $addon->getName(),
|
||||
'enabled' => $addon->isEnabled(),
|
||||
])->values();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Controller;
|
||||
use Illuminate\View\View;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use stdClass;
|
||||
|
||||
class CreditsController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$all_modules = Module::all();
|
||||
$all_modules = app(AddonRegistry::class)->all()->keyBy(fn ($addon): string => $addon->getName());
|
||||
$v7_defaults = ['Awards', 'Vacentral', 'Sample'];
|
||||
$modules = collect();
|
||||
|
||||
@ -21,7 +22,7 @@ class CreditsController extends Controller
|
||||
|
||||
$module_details = $this->ReadModuleJson($key);
|
||||
|
||||
if ($module_details instanceof \stdClass) {
|
||||
if ($module_details instanceof stdClass) {
|
||||
$modules->push($module_details);
|
||||
}
|
||||
}
|
||||
@ -33,7 +34,7 @@ class CreditsController extends Controller
|
||||
|
||||
// Read module.json file
|
||||
// Return laravel collection
|
||||
public function ReadModuleJson($module_name = null): ?\stdClass
|
||||
public function ReadModuleJson($module_name = null): ?stdClass
|
||||
{
|
||||
$file = isset($module_name) ? base_path().'/modules/'.$module_name.'/module.json' : null;
|
||||
|
||||
@ -43,14 +44,14 @@ class CreditsController extends Controller
|
||||
|
||||
$contents = json_decode(file_get_contents($file));
|
||||
|
||||
$details = new \stdClass();
|
||||
$details = new stdClass();
|
||||
$details->name = $contents->name ?? $module_name;
|
||||
$details->description = $contents->description ?? null;
|
||||
$details->version = $contents->version ?? null;
|
||||
$details->readme_url = $contents->readme_url ?? null;
|
||||
$details->license_url = $contents->license_url ?? null;
|
||||
$details->attribution = $contents->attribution ?? null;
|
||||
$details->active = Module::isEnabled($contents->name);
|
||||
$details->active = (bool) app(AddonRegistry::class)->find($contents->name)?->isEnabled();
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Controller;
|
||||
use App\Models\Airline;
|
||||
use App\Models\File;
|
||||
@ -11,8 +12,6 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
use Laracasts\Flash\Flash;
|
||||
use Nwidart\Modules\Exceptions\ModuleNotFoundException;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
@ -67,16 +66,13 @@ class DownloadController extends Controller
|
||||
}
|
||||
|
||||
// See if they inserted a link to the ACARS download
|
||||
try {
|
||||
Module::findOrFail('VMSAcars');
|
||||
if (app(AddonRegistry::class)->find('VMSAcars')) {
|
||||
$downloadUrl = DB::table('vmsacars_config')->where(['id' => 'download_url'])->first();
|
||||
if (!empty($downloadUrl) && !empty($downloadUrl->value)) {
|
||||
$regrouped_files['ACARS'] = collect([
|
||||
new File(['id' => 'vmsacars', 'name' => 'ACARS Client', 'disk' => null, 'path' => $downloadUrl->value]),
|
||||
]);
|
||||
}
|
||||
} catch (ModuleNotFoundException) {
|
||||
// noop, don't insert the ACARS download
|
||||
}
|
||||
|
||||
ksort($regrouped_files, SORT_STRING);
|
||||
@ -93,8 +89,7 @@ class DownloadController extends Controller
|
||||
{
|
||||
// See if they're trying to download the ACARS client
|
||||
if ($id === 'vmsacars' && Auth::check()) {
|
||||
try {
|
||||
Module::find('VMSAcars');
|
||||
if (app(AddonRegistry::class)->find('VMSAcars')) {
|
||||
$downloadUrl = DB::table('vmsacars_config')
|
||||
->where(['id' => 'download_url'])
|
||||
->first();
|
||||
@ -102,7 +97,6 @@ class DownloadController extends Controller
|
||||
if (!empty($downloadUrl) && !empty($downloadUrl->value)) {
|
||||
return redirect()->to($downloadUrl->value);
|
||||
}
|
||||
} catch (ModuleNotFoundException) {
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Controller;
|
||||
use App\Events\ProfileUpdated;
|
||||
use App\Models\Airline;
|
||||
@ -24,7 +25,6 @@ use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Laracasts\Flash\Flash;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
@ -41,14 +41,12 @@ class ProfileController extends Controller
|
||||
private function acarsEnabled(): bool
|
||||
{
|
||||
// Is the ACARS module enabled?
|
||||
$acars_enabled = false;
|
||||
/** @var ?\Nwidart\Modules\Module $acars */
|
||||
$acars = Module::find('VMSAcars');
|
||||
$acars = app(AddonRegistry::class)->find('VMSAcars');
|
||||
if ($acars) {
|
||||
return $acars->isEnabled();
|
||||
}
|
||||
|
||||
return $acars_enabled;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Override;
|
||||
use Str;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@ -69,6 +70,15 @@ class Addon extends Model
|
||||
'installed_at' => 'nullable|date',
|
||||
];
|
||||
|
||||
#[Override]
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
'installed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an addon from a manifest
|
||||
*
|
||||
@ -105,13 +115,25 @@ class Addon extends Model
|
||||
return $addon;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function casts(): array
|
||||
/**
|
||||
* Return the lower-cased addon name
|
||||
*/
|
||||
public function getLowerName(): string
|
||||
{
|
||||
return [
|
||||
'enabled' => 'boolean',
|
||||
'installed_at' => 'datetime',
|
||||
];
|
||||
return strtolower((string) $this->name);
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the studly-cased addon name
|
||||
*/
|
||||
public function getStudlyName(): string
|
||||
{
|
||||
return Str::studly($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,6 +174,6 @@ class Addon extends Model
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) $this->enabled;
|
||||
return $this->enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,11 @@ use App\Casts\DistanceCast;
|
||||
use App\Contracts\Model;
|
||||
use App\Enums\FlightType;
|
||||
use App\Support\Days;
|
||||
use BackedEnum;
|
||||
use Database\Factories\FlightFactory;
|
||||
use App\Traits\HasNanoIds;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@ -19,12 +22,17 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Kyslik\ColumnSortable\Sortable;
|
||||
use Override;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Stringable;
|
||||
use UnitEnum;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property int|null $bundle_id
|
||||
* @property FlightBundle|null $bundle
|
||||
* @property int $airline_id
|
||||
* @property int $flight_number
|
||||
* @property string|null $callsign
|
||||
@ -55,8 +63,6 @@ use Spatie\Activitylog\Traits\LogsActivity;
|
||||
* @property bool $visible
|
||||
* @property int|null $event_id
|
||||
* @property int|null $user_id
|
||||
* @property int|null $bundle_id
|
||||
* @property FlightBundle|null $bundle
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Carbon|null $deleted_at
|
||||
@ -81,64 +87,65 @@ use Spatie\Activitylog\Traits\LogsActivity;
|
||||
* @property-read int|null $subfleets_count
|
||||
* @property-read User|null $user
|
||||
*
|
||||
* @method static Builder<static>|Flight active()
|
||||
* @method static Builder<static>|Flight distanceAtLeast(int $distance)
|
||||
* @method static Builder<static>|Flight distanceAtMost(int $distance)
|
||||
* @method static \Database\Factories\FlightFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|Flight flightTimeAtLeast(int $minutes)
|
||||
* @method static Builder<static>|Flight flightTimeAtMost(int $minutes)
|
||||
* @method static Builder<static>|Flight forAirline(int $airlineId)
|
||||
* @method static Builder<static>|Flight forTypeRating(int $typeRatingId)
|
||||
* @method static Builder<static>|Flight fromAirport(string $icao)
|
||||
* @method static Builder<static>|Flight newModelQuery()
|
||||
* @method static Builder<static>|Flight newQuery()
|
||||
* @method static Builder<static>|Flight onlyTrashed()
|
||||
* @method static Builder<static>|Flight query()
|
||||
* @method static Builder<static>|Flight sortable($defaultParameters = null)
|
||||
* @method static Builder<static>|Flight toAirport(string $icao)
|
||||
* @method static Builder<static>|Flight visible()
|
||||
* @method static Builder<static>|Flight whereEnabled($value)
|
||||
* @method static Builder<static>|Flight whereAirlineId($value)
|
||||
* @method static Builder<static>|Flight whereAltAirportId($value)
|
||||
* @method static Builder<static>|Flight whereArrAirportId($value)
|
||||
* @method static Builder<static>|Flight whereArrTime($value)
|
||||
* @method static Builder<static>|Flight whereCallsign($value)
|
||||
* @method static Builder<static>|Flight whereCreatedAt($value)
|
||||
* @method static Builder<static>|Flight whereDays($value)
|
||||
* @method static Builder<static>|Flight whereDeletedAt($value)
|
||||
* @method static Builder<static>|Flight whereDistance($value)
|
||||
* @method static Builder<static>|Flight whereDptAirportId($value)
|
||||
* @method static Builder<static>|Flight whereDptTime($value)
|
||||
* @method static Builder<static>|Flight whereEndDate($value)
|
||||
* @method static Builder<static>|Flight whereEventId($value)
|
||||
* @method static Builder<static>|Flight whereFlightNumber($value)
|
||||
* @method static Builder<static>|Flight whereFlightTime($value)
|
||||
* @method static Builder<static>|Flight whereFlightType($value)
|
||||
* @method static Builder<static>|Flight whereHasBid($value)
|
||||
* @method static Builder<static>|Flight whereId($value)
|
||||
* @method static Builder<static>|Flight whereLevel($value)
|
||||
* @method static Builder<static>|Flight whereLoadFactor($value)
|
||||
* @method static Builder<static>|Flight whereLoadFactorVariance($value)
|
||||
* @method static Builder<static>|Flight whereNotes($value)
|
||||
* @method static Builder<static>|Flight whereOwnerId($value)
|
||||
* @method static Builder<static>|Flight whereOwnerType($value)
|
||||
* @method static Builder<static>|Flight wherePilotPay($value)
|
||||
* @method static Builder<static>|Flight whereRoute($value)
|
||||
* @method static Builder<static>|Flight whereRouteCode($value)
|
||||
* @method static Builder<static>|Flight whereRouteLeg($value)
|
||||
* @method static Builder<static>|Flight whereScheduled($value)
|
||||
* @method static Builder<static>|Flight whereStartDate($value)
|
||||
* @method static Builder<static>|Flight whereUpdatedAt($value)
|
||||
* @method static Builder<static>|Flight whereUserId($value)
|
||||
* @method static Builder<static>|Flight whereVisible($value)
|
||||
* @method static Builder<static>|Flight withFlightType(string $type)
|
||||
* @method static Builder<static>|Flight withIcaoType(string $icao)
|
||||
* @method static Builder<static>|Flight withSubfleet(int $subfleetId)
|
||||
* @method static Builder<static>|Flight withTrashed(bool $withTrashed = true)
|
||||
* @method static Builder<static>|Flight withoutTrashed()
|
||||
* @method static Builder<static>|Flight active()
|
||||
* @method static Builder<static>|Flight distanceAtLeast(int $distance)
|
||||
* @method static Builder<static>|Flight distanceAtMost(int $distance)
|
||||
* @method static FlightFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|Flight flightTimeAtLeast(int $minutes)
|
||||
* @method static Builder<static>|Flight flightTimeAtMost(int $minutes)
|
||||
* @method static Builder<static>|Flight forAirline(int $airlineId)
|
||||
* @method static Builder<static>|Flight forTypeRating(int $typeRatingId)
|
||||
* @method static Builder<static>|Flight fromAirport(string $icao)
|
||||
* @method static Builder<static>|Flight newModelQuery()
|
||||
* @method static Builder<static>|Flight newQuery()
|
||||
* @method static Builder<static>|Flight onlyTrashed()
|
||||
* @method static Builder<static>|Flight query()
|
||||
* @method static Builder<static>|Flight sortable($defaultParameters = null)
|
||||
* @method static Builder<static>|Flight toAirport(string $icao)
|
||||
* @method static Builder<static>|Flight visible()
|
||||
* @method static Builder<static>|Flight whereEnabled($value)
|
||||
* @method static Builder<static>|Flight whereAirlineId($value)
|
||||
* @method static Builder<static>|Flight whereAltAirportId($value)
|
||||
* @method static Builder<static>|Flight whereArrAirportId($value)
|
||||
* @method static Builder<static>|Flight whereArrTime($value)
|
||||
* @method static Builder<static>|Flight whereCallsign($value)
|
||||
* @method static Builder<static>|Flight whereCreatedAt($value)
|
||||
* @method static Builder<static>|Flight whereDays($value)
|
||||
* @method static Builder<static>|Flight whereDeletedAt($value)
|
||||
* @method static Builder<static>|Flight whereDistance($value)
|
||||
* @method static Builder<static>|Flight whereDptAirportId($value)
|
||||
* @method static Builder<static>|Flight whereDptTime($value)
|
||||
* @method static Builder<static>|Flight whereEndDate($value)
|
||||
* @method static Builder<static>|Flight whereEventId($value)
|
||||
* @method static Builder<static>|Flight whereFlightNumber($value)
|
||||
* @method static Builder<static>|Flight whereFlightTime($value)
|
||||
* @method static Builder<static>|Flight whereFlightType($value)
|
||||
* @method static Builder<static>|Flight whereHasBid($value)
|
||||
* @method static Builder<static>|Flight whereId($value)
|
||||
* @method static Builder<static>|Flight whereLevel($value)
|
||||
* @method static Builder<static>|Flight whereLoadFactor($value)
|
||||
* @method static Builder<static>|Flight whereLoadFactorVariance($value)
|
||||
* @method static Builder<static>|Flight whereNotes($value)
|
||||
* @method static Builder<static>|Flight whereOwnerId($value)
|
||||
* @method static Builder<static>|Flight whereOwnerType($value)
|
||||
* @method static Builder<static>|Flight wherePilotPay($value)
|
||||
* @method static Builder<static>|Flight whereRoute($value)
|
||||
* @method static Builder<static>|Flight whereRouteCode($value)
|
||||
* @method static Builder<static>|Flight whereRouteLeg($value)
|
||||
* @method static Builder<static>|Flight whereScheduled($value)
|
||||
* @method static Builder<static>|Flight whereStartDate($value)
|
||||
* @method static Builder<static>|Flight whereUpdatedAt($value)
|
||||
* @method static Builder<static>|Flight whereUserId($value)
|
||||
* @method static Builder<static>|Flight whereVisible($value)
|
||||
* @method static Builder<static>|Flight withFlightType(string $type)
|
||||
* @method static Builder<static>|Flight withIcaoType(string $icao)
|
||||
* @method static Builder<static>|Flight withSubfleet(int $subfleetId)
|
||||
* @method static Builder<static>|Flight withTrashed(bool $withTrashed = true)
|
||||
* @method static Builder<static>|Flight withoutTrashed()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
#[WithoutIncrementing]
|
||||
class Flight extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
@ -149,13 +156,13 @@ class Flight extends Model
|
||||
|
||||
public $table = 'flights';
|
||||
|
||||
/** The form wants this */
|
||||
public $hours;
|
||||
|
||||
public $minutes;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'bundle_id',
|
||||
'airline_id',
|
||||
'flight_number',
|
||||
'callsign',
|
||||
@ -185,7 +192,6 @@ class Flight extends Model
|
||||
'visible',
|
||||
'event_id',
|
||||
'user_id',
|
||||
'bundle_id',
|
||||
'owner_type',
|
||||
'owner_id',
|
||||
];
|
||||
@ -214,7 +220,7 @@ class Flight extends Model
|
||||
'fares_count',
|
||||
];
|
||||
|
||||
#[\Override]
|
||||
#[Override]
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
@ -421,11 +427,11 @@ class Flight extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof \BackedEnum) {
|
||||
if ($value instanceof BackedEnum) {
|
||||
$string = (string) $value->value;
|
||||
} elseif ($value instanceof \UnitEnum) {
|
||||
} elseif ($value instanceof UnitEnum) {
|
||||
$string = $value->name;
|
||||
} elseif (is_scalar($value) || $value instanceof \Stringable) {
|
||||
} elseif (is_scalar($value) || $value instanceof Stringable) {
|
||||
$string = (string) $value;
|
||||
} else {
|
||||
// Non-stringable object — defer to PHP's cast and let it raise.
|
||||
|
||||
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Addons\AddonAutoLoader;
|
||||
use App\Addons\Compat\ModuleRepository;
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Addons\Filament\FilamentPanelExtender;
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\AutoloadGuard;
|
||||
@ -15,18 +15,16 @@ use Illuminate\Support\ServiceProvider;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* Wires the Phase 2 addon engine into application boot.
|
||||
* Wires the addon engine into application boot.
|
||||
*
|
||||
* register() handles all engine wiring so that addon service providers are
|
||||
* registered before Filament panels resolve (D2-10):
|
||||
* 1. Binds all engine singletons (Octane-safe: stateless services).
|
||||
* 2. Binds the 'modules' container key to ModuleRepository, making the
|
||||
* nwidart Module facade route to our shim (nwidart provider retired).
|
||||
* 3. At non-console boot: runs the classmap-authoritative guard (LOAD-08).
|
||||
* 2. At non-console boot: runs the classmap-authoritative guard (LOAD-08).
|
||||
* Skipped in console (D-17) to avoid blocking migrate/install/tests.
|
||||
* 4. Runs the AddonLoader in all contexts so artisan sees addon
|
||||
* 3. Runs the AddonLoader in all contexts so artisan sees addon
|
||||
* commands and migrations (D2-11). No-ops on empty cache.
|
||||
* 5. Hooks FilamentPanelExtender into beforeResolving('filament', ...)
|
||||
* 4. Hooks FilamentPanelExtender into beforeResolving('filament', ...)
|
||||
* so addon Filament discovery is applied before panels resolve (D2-07).
|
||||
*
|
||||
* boot() auto-primes the boot cache when absent/stale (D2-09). This is the
|
||||
@ -54,11 +52,7 @@ class AddonServiceProvider extends ServiceProvider
|
||||
// ── Phase 2 singletons ──────────────────────────────────────────────
|
||||
$this->app->singleton(AddonAutoLoader::class);
|
||||
$this->app->singleton(FilamentPanelExtender::class);
|
||||
$this->app->singleton(ModuleRepository::class);
|
||||
|
||||
// Bind the nwidart 'modules' container key to our shim so that
|
||||
// Nwidart\Modules\Facades\Module resolves to ModuleRepository.
|
||||
$this->app->singleton('modules', fn ($app) => $app->make(ModuleRepository::class));
|
||||
$this->app->singleton(AddonRegistry::class);
|
||||
|
||||
// ── Non-console: autoload guard ─────────────────────────────────────
|
||||
// D-17: skip in console contexts (migrate/install/tests).
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Nwidart\Modules\LaravelModulesServiceProvider as BaseModulesServiceProvider;
|
||||
|
||||
class ModulesServiceProvider extends BaseModulesServiceProvider
|
||||
{
|
||||
#[\Override]
|
||||
public function register(): void
|
||||
{
|
||||
parent::register();
|
||||
|
||||
// Boot the modules before resolving Filament so that modules' panels can be discovered
|
||||
$this->app->beforeResolving('filament', function (): void {
|
||||
parent::boot();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Award;
|
||||
use App\Contracts\Service;
|
||||
use App\Support\ClassLoader;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
class AwardService extends Service
|
||||
{
|
||||
@ -26,7 +26,7 @@ class AwardService extends Service
|
||||
// $awards = array_merge($awards, $classes);
|
||||
|
||||
// Look throughout all the other modules, in the module/{MODULE}/Awards directory
|
||||
foreach (Module::all() as $module) {
|
||||
foreach (app(AddonRegistry::class)->all() as $module) {
|
||||
$path = $module->getExtraPath('Awards');
|
||||
$classes = ClassLoader::getClassesInPath($path);
|
||||
|
||||
|
||||
@ -4,13 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Installer;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Service;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
class MigrationService extends Service
|
||||
{
|
||||
@ -40,7 +41,7 @@ class MigrationService extends Service
|
||||
'core' => App::databasePath().'/'.$dir,
|
||||
];
|
||||
|
||||
$modules = Module::allEnabled();
|
||||
$modules = app(AddonRegistry::class)->enabled();
|
||||
foreach ($modules as $module) {
|
||||
$module_path = $module->getPath().'/Database/'.$dir;
|
||||
if (file_exists($module_path)) {
|
||||
@ -93,7 +94,7 @@ class MigrationService extends Service
|
||||
return trim(Artisan::output());
|
||||
}
|
||||
|
||||
public function runAllMigrationsWithStreaming(\Closure $streamCallback): void
|
||||
public function runAllMigrationsWithStreaming(Closure $streamCallback): void
|
||||
{
|
||||
$command = ['migrate', '--force', '--realpath'];
|
||||
|
||||
@ -147,7 +148,7 @@ class MigrationService extends Service
|
||||
return trim(Artisan::output());
|
||||
}
|
||||
|
||||
public function runAllDataMigrationsWithStreaming(\Closure $streamCallback): void
|
||||
public function runAllDataMigrationsWithStreaming(Closure $streamCallback): void
|
||||
{
|
||||
$command = ['migrate-data', '--force', '--realpath'];
|
||||
|
||||
|
||||
@ -4,16 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Addons\Compat\Module;
|
||||
use App\Addons\Compat\ModuleRepository;
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Service;
|
||||
|
||||
class ModuleService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ModuleRepository $modules
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Module-registered admin nav links. Populated once per worker via each
|
||||
* module's boot()->registerLinks() call; not a per-request accumulator.
|
||||
@ -69,43 +64,26 @@ class ModuleService extends Service
|
||||
}
|
||||
|
||||
/**
|
||||
* Update module with the status passed by user
|
||||
* TODO: Remove
|
||||
* Update module with the status passed by user.
|
||||
*
|
||||
* @deprecated Delegate to AddonRegistry::enable()/disable() directly.
|
||||
*/
|
||||
public function updateModule(string $name, bool $enabled): void
|
||||
{
|
||||
$module = $this->modules->find($name);
|
||||
|
||||
if (!$module) {
|
||||
return;
|
||||
}
|
||||
|
||||
// setActive() flips the enabled flag, persists to DB, and regenerates the boot cache
|
||||
// (via ModuleShim::setActive() → AddonRuntimeService::run()). The per-module migrate command
|
||||
// (module:migrate) belonged to nwidart and no longer exists. Addon migration execution
|
||||
// is owned by the standard `php artisan migrate` path (Phase 5 lifecycle).
|
||||
$module->setActive($enabled);
|
||||
|
||||
if (file_exists(base_path('bootstrap/cache/modules.php'))) {
|
||||
unlink(base_path('bootstrap/cache/modules.php'));
|
||||
if ($enabled) {
|
||||
app(AddonRegistry::class)->enable($name);
|
||||
} else {
|
||||
app(AddonRegistry::class)->disable($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Module from the Storage & Database.
|
||||
*
|
||||
* @deprecated Delegate to AddonRegistry::delete() directly.
|
||||
*/
|
||||
public function deleteModule(string $name): void
|
||||
{
|
||||
$module = $this->modules->find($name);
|
||||
|
||||
if (!$module) {
|
||||
return;
|
||||
}
|
||||
|
||||
$module->delete();
|
||||
|
||||
if (file_exists(base_path('bootstrap/cache/modules.php'))) {
|
||||
unlink(base_path('bootstrap/cache/modules.php'));
|
||||
}
|
||||
app(AddonRegistry::class)->delete($name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,12 +43,9 @@ use App\Support\Units\Fuel;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
class PirepService extends Service
|
||||
{
|
||||
@ -231,6 +228,7 @@ class PirepService extends Service
|
||||
*/
|
||||
public function update(string $pirep_id, array $attrs, array $fields = [], array $fares = []): Pirep
|
||||
{
|
||||
/** @var Pirep $pirep */
|
||||
$pirep = Pirep::findOrFail($pirep_id);
|
||||
$pirep->update($attrs);
|
||||
$pirep->refresh();
|
||||
@ -712,69 +710,10 @@ class PirepService extends Service
|
||||
|
||||
$pirep->loadMissing('aircraft', 'flight', 'user');
|
||||
$aircraft = $pirep->aircraft;
|
||||
$flight = $pirep->flight;
|
||||
$user = $pirep->user;
|
||||
|
||||
event(new PirepDiverted($pirep));
|
||||
|
||||
/** @var ?\Nwidart\Modules\Module $has_vmsacars */
|
||||
$has_vmsacars = Module::find('VMSAcars');
|
||||
|
||||
$has_vmsacars_config = Schema::hasTable('vmsacars_config');
|
||||
|
||||
if ($has_vmsacars && $has_vmsacars_config && $flight) {
|
||||
/** @var ?object $query */
|
||||
$query = DB::table('vmsacars_config')->find('disable_free_flights');
|
||||
$free_flights_disabled = $query?->value;
|
||||
// Log::debug('vmsAcars | Disable Free Flights Setting: '.$free_flights_disabled.', considered as '.get_truth_state($free_flights_disabled));
|
||||
|
||||
if (get_truth_state($free_flights_disabled)) {
|
||||
$repositionAttributes = [
|
||||
'airline_id' => $flight->airline_id,
|
||||
'flight_number' => $flight->flight_number,
|
||||
'callsign' => $flight->callsign,
|
||||
'route_code' => PirepStatus::DIVERTED,
|
||||
'dpt_airport_id' => $diversion_airport->id,
|
||||
'arr_airport_id' => $pirep->arr_airport_id,
|
||||
'user_id' => $user->id,
|
||||
];
|
||||
|
||||
$lockKey = implode(':', [
|
||||
'diversion-flight',
|
||||
$flight->airline_id,
|
||||
$flight->flight_number,
|
||||
$diversion_airport->id,
|
||||
$pirep->arr_airport_id,
|
||||
$user->id,
|
||||
]);
|
||||
|
||||
/** @var Flight $repositionFlight */
|
||||
$repositionFlight = Cache::lock($lockKey, 10)->block(5, function () use ($aircraft, $diversion_airport, $flight, $pirep, $repositionAttributes) {
|
||||
$repositionFlight = Flight::query()->firstOrCreate(
|
||||
$repositionAttributes,
|
||||
[
|
||||
'distance' => $this->airportSvc->calculateDistance($diversion_airport->id, $pirep->arr_airport_id),
|
||||
'flight_time' => 1,
|
||||
'flight_type' => $flight->flight_type,
|
||||
'notes' => 'DIVERTED FLIGHT RE-POSITIONING TO DESTINATION',
|
||||
'visible' => true,
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$repositionFlight->subfleets()->syncWithoutDetaching([$aircraft->subfleet_id]);
|
||||
|
||||
return $repositionFlight;
|
||||
});
|
||||
|
||||
if ($repositionFlight->wasRecentlyCreated) {
|
||||
Log::info('Diversion repositioning flight '.$repositionFlight->id.' from '.$diversion_airport->id.' to '.$pirep->arr_airport_id.' created');
|
||||
} else {
|
||||
Log::info('Diversion repositioning flight '.$repositionFlight->id.' from '.$diversion_airport->id.' to '.$pirep->arr_airport_id.' reused');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (setting('notifications.discord_pirep_diverted', false)) {
|
||||
Notification::send([$pirep], new PirepDiverted($pirep));
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Contracts\Model;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Support\Str;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
use Pdp\Rules;
|
||||
|
||||
/**
|
||||
@ -72,8 +72,8 @@ class Utils
|
||||
*/
|
||||
public static function installerEnabled()
|
||||
{
|
||||
/** @var ?\Nwidart\Modules\Module $installer */
|
||||
$installer = Module::find('installer');
|
||||
$installer = app(AddonRegistry::class)->find('installer');
|
||||
|
||||
if (!$installer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
46
tests/Feature/Addons/AddonDiscoveryRebuildTest.php
Normal file
46
tests/Feature/Addons/AddonDiscoveryRebuildTest.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\BootCache;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
beforeEach(function (): void {
|
||||
// The addons migration seeds bundled module rows; clear them so each test
|
||||
// starts with a known-empty state.
|
||||
Addon::query()->delete();
|
||||
|
||||
$this->base = sys_get_temp_dir().'/rebuild-'.uniqid();
|
||||
$this->addonDir = $this->base.'/Demo';
|
||||
File::ensureDirectoryExists($this->addonDir);
|
||||
File::put($this->addonDir.'/module.json', json_encode(['name' => 'Demo', 'providers' => []]));
|
||||
File::put($this->addonDir.'/composer.json', json_encode(['autoload' => ['psr-4' => ['Modules\\Demo\\' => '']]]));
|
||||
Config::set('addons.paths.base', $this->base);
|
||||
|
||||
app(BootCache::class)->delete();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
app(BootCache::class)->delete();
|
||||
File::deleteDirectory($this->base);
|
||||
});
|
||||
|
||||
it('rebuildCache() writes enabled addons to the boot cache', function (): void {
|
||||
Addon::factory()->create(['name' => 'Demo', 'path' => $this->addonDir, 'enabled' => true]);
|
||||
|
||||
app(AddonDiscoveryService::class)->rebuildCache();
|
||||
|
||||
$names = app(BootCache::class)->enabled()->map(fn ($e): string => $e->name)->all();
|
||||
expect($names)->toContain('Demo');
|
||||
});
|
||||
|
||||
it('rebuildCache() omits disabled addons', function (): void {
|
||||
Addon::factory()->create(['name' => 'Demo', 'path' => $this->addonDir, 'enabled' => false]);
|
||||
|
||||
app(AddonDiscoveryService::class)->rebuildCache();
|
||||
|
||||
expect(app(BootCache::class)->enabled())->toHaveCount(0);
|
||||
});
|
||||
46
tests/Feature/Addons/AddonRegistryLifecycleTest.php
Normal file
46
tests/Feature/Addons/AddonRegistryLifecycleTest.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Models\Addon;
|
||||
|
||||
beforeEach(function (): void {
|
||||
// The addons migration seeds bundled module rows; clear them so each test
|
||||
// starts with a known-empty state.
|
||||
Addon::query()->delete();
|
||||
|
||||
$this->registry = app(AddonRegistry::class);
|
||||
});
|
||||
|
||||
it('enable() sets the addon enabled', function (): void {
|
||||
Addon::factory()->create(['name' => 'Awards', 'path' => '/m/Awards', 'enabled' => false]);
|
||||
|
||||
$this->registry->enable('Awards');
|
||||
|
||||
expect(Addon::query()->where('name', 'Awards')->first()->enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
it('disable() clears the addon enabled flag', function (): void {
|
||||
Addon::factory()->create(['name' => 'Awards', 'path' => '/m/Awards', 'enabled' => true]);
|
||||
|
||||
$this->registry->disable('Awards');
|
||||
|
||||
expect(Addon::query()->where('name', 'Awards')->first()->enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('delete() removes the DB row', function (): void {
|
||||
Addon::factory()->create(['name' => 'Awards', 'path' => '/m/Awards', 'enabled' => false]);
|
||||
|
||||
$this->registry->delete('Awards');
|
||||
|
||||
expect(Addon::query()->where('name', 'Awards')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('enable()/disable()/delete() no-op on an unknown addon', function (): void {
|
||||
$this->registry->enable('Nope');
|
||||
$this->registry->disable('Nope');
|
||||
$this->registry->delete('Nope');
|
||||
|
||||
expect(Addon::query()->count())->toBe(0);
|
||||
});
|
||||
44
tests/Feature/Addons/AddonRegistryReadTest.php
Normal file
44
tests/Feature/Addons/AddonRegistryReadTest.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Addons\AddonRegistry;
|
||||
use App\Exceptions\AddonNotFoundException;
|
||||
use App\Models\Addon;
|
||||
|
||||
beforeEach(function (): void {
|
||||
// The addons migration seeds bundled module rows; clear them so each test
|
||||
// starts with a known-empty state.
|
||||
Addon::query()->delete();
|
||||
|
||||
$this->registry = app(AddonRegistry::class);
|
||||
});
|
||||
|
||||
it('find() returns an addon by name', function (): void {
|
||||
Addon::factory()->create(['name' => 'VMSAcars', 'path' => '/m/VMSAcars']);
|
||||
|
||||
expect($this->registry->find('VMSAcars'))->not->toBeNull()
|
||||
->and($this->registry->find('VMSAcars')->getName())->toBe('VMSAcars');
|
||||
});
|
||||
|
||||
it('find() returns null when no addon matches', function (): void {
|
||||
expect($this->registry->find('Nope'))->toBeNull();
|
||||
});
|
||||
|
||||
it('find() matches a null-name row by its path basename', function (): void {
|
||||
Addon::factory()->create(['name' => null, 'path' => '/m/Awards']);
|
||||
|
||||
expect($this->registry->find('Awards'))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('findOrFail() throws when no addon matches', function (): void {
|
||||
$this->registry->findOrFail('Nope');
|
||||
})->throws(AddonNotFoundException::class);
|
||||
|
||||
it('enabled() returns only enabled addons', function (): void {
|
||||
Addon::factory()->create(['enabled' => true]);
|
||||
Addon::factory()->create(['enabled' => false]);
|
||||
|
||||
expect($this->registry->enabled())->toHaveCount(1)
|
||||
->and($this->registry->enabled()->first()->isEnabled())->toBeTrue();
|
||||
});
|
||||
34
tests/Feature/Addons/AddonsPageTest.php
Normal file
34
tests/Feature/Addons/AddonsPageTest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\BootCache;
|
||||
use App\Filament\Pages\Addons;
|
||||
use Database\Seeders\ShieldSeeder;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->seed(ShieldSeeder::class);
|
||||
$this->actingAs(createAdminUser());
|
||||
|
||||
// Ensure the boot cache and DB are seeded with the bundled modules.
|
||||
app(BootCache::class)->delete();
|
||||
app(AddonDiscoveryService::class)->run();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
app(BootCache::class)->delete();
|
||||
});
|
||||
|
||||
it('lists addons from AddonRegistry on the page', function (): void {
|
||||
Livewire::test(Addons::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('Sample');
|
||||
});
|
||||
|
||||
it('shows enabled status for addons', function (): void {
|
||||
Livewire::test(Addons::class)
|
||||
->assertSuccessful()
|
||||
->assertSee('Sample');
|
||||
});
|
||||
@ -1,264 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Addons\Compat\Module;
|
||||
use App\Addons\Compat\ModuleRepository;
|
||||
use App\Addons\Models\AddonBootCache;
|
||||
use App\Addons\Services\AddonDiscoveryService;
|
||||
use App\Addons\Support\BootCache;
|
||||
use App\Addons\Support\ManifestParser;
|
||||
use App\Models\Addon;
|
||||
use Nwidart\Modules\Exceptions\ModuleNotFoundException;
|
||||
|
||||
beforeEach(function (): void {
|
||||
// Ensure a fresh boot cache for each test.
|
||||
app(BootCache::class)->delete();
|
||||
|
||||
// Seed DB rows + boot cache via AddonRuntimeService so tests start from a known state.
|
||||
app(AddonDiscoveryService::class)->run();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
app(BootCache::class)->delete();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 1. all() returns a collection keyed by name
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('all() returns collection keyed by module name containing the 3 bundled modules', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$all = $repo->all();
|
||||
|
||||
expect($all)->toHaveKey('Sample')
|
||||
->toHaveKey('Awards')
|
||||
->toHaveKey('VMSAcars');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 2. find() returns a shim with correct attributes
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('find(Sample) returns a shim with correct name, path, enabled, and lowerName', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->find('Sample');
|
||||
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->getName())->toBe('Sample')
|
||||
->and($shim->getLowerName())->toBe('sample')
|
||||
->and($shim->isEnabled())->toBeTrue()
|
||||
->and($shim->getPath())->toEndWith('modules/Sample');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 3. find() returns null for unknown module
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('find(DoesNotExist) returns null', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect($repo->find('DoesNotExist'))->toBeNull();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 4. findOrFail() returns shim or throws
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('findOrFail(Sample) returns a shim', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->findOrFail('Sample');
|
||||
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->getName())->toBe('Sample');
|
||||
});
|
||||
|
||||
it('findOrFail(Nope) throws ModuleNotFoundException', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect(fn () => $repo->findOrFail('Nope'))
|
||||
->toThrow(ModuleNotFoundException::class);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 5. allEnabled() excludes a disabled addon
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('allEnabled() excludes an addon after setActive(false) and re-prime', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
$shim = $repo->find('Sample');
|
||||
expect($shim)->not->toBeNull();
|
||||
|
||||
// Disable via shim (also re-primes the cache).
|
||||
$shim->setActive(false);
|
||||
|
||||
// Re-resolve repo to get fresh state.
|
||||
$repo2 = app(ModuleRepository::class);
|
||||
$enabled = $repo2->allEnabled();
|
||||
|
||||
expect($enabled)->not->toHaveKey('Sample')
|
||||
->toHaveKey('Awards')
|
||||
->toHaveKey('VMSAcars');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 6. isEnabled()
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('isEnabled(Sample) returns true', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect($repo->isEnabled('Sample'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('isEnabled(Nope) returns false', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect($repo->isEnabled('Nope'))->toBeFalse();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 7. getExtraPath()
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('getExtraPath(Awards) returns getPath()/Awards', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->find('Sample');
|
||||
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->getExtraPath('Awards'))->toBe($shim->getPath().'/Awards');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 8. config()
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('config(namespace) returns Modules', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect($repo->config('namespace'))->toBe('Modules');
|
||||
});
|
||||
|
||||
it('config(unknown, default) returns the default', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
|
||||
expect($repo->config('something', 'x'))->toBe('x');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 9. setActive() persists to DB and regenerates boot cache
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('setActive(false) updates DB and removes Sample from boot cache; setActive(true) re-includes it', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$runtime = app(BootCache::class);
|
||||
|
||||
$shim = $repo->find('Sample');
|
||||
expect($shim)->not->toBeNull();
|
||||
|
||||
// Disable.
|
||||
$shim->setActive(false);
|
||||
|
||||
// DB check.
|
||||
$addonRow = Addon::query()->where('path', 'LIKE', '%modules/Sample')->first();
|
||||
expect($addonRow)->not->toBeNull()
|
||||
->and($addonRow->enabled)->toBeFalse();
|
||||
|
||||
// Boot cache check — Sample must be absent.
|
||||
$cached = $runtime->read();
|
||||
$names = array_map(fn (AddonBootCache $r): string => $r->name, $cached);
|
||||
expect($names)->not->toContain('Sample');
|
||||
|
||||
// Re-enable.
|
||||
$shim->setActive(true);
|
||||
|
||||
$addonRow->refresh();
|
||||
expect($addonRow->enabled)->toBeTrue();
|
||||
|
||||
// Boot cache must include Sample again.
|
||||
$cached2 = $runtime->read();
|
||||
$names2 = array_map(fn (AddonBootCache $r): string => $r->name, $cached2);
|
||||
expect($names2)->toContain('Sample');
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 10. name/description property access via __get
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('$shim->name equals getName()', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->find('Sample');
|
||||
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->name)->toBe('Sample');
|
||||
});
|
||||
|
||||
it('$shim->description returns null when manifest description is blank', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->find('Sample');
|
||||
|
||||
// Sample module.json has description: "" which normalises to null.
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->description)->toBeNull();
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 11. getStudlyName() returns StudlyCase of the module name
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('getStudlyName() returns StudlyCase of the module name', function (): void {
|
||||
$repo = app(ModuleRepository::class);
|
||||
$shim = $repo->find('Sample');
|
||||
|
||||
expect($shim)->not->toBeNull()
|
||||
->and($shim->getStudlyName())->toBe('Sample')
|
||||
// StudlyCase is always ≥ the lowercase equivalent.
|
||||
->and($shim->getStudlyName())->not->toBe($shim->getLowerName());
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 12. delete() removes DB row and boot cache no longer lists the addon
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
it('delete() removes the Addon DB row and the boot cache no longer lists it', function (): void {
|
||||
// Create a temporary addon directory with a valid module.json so AddonRuntimeService
|
||||
// can discover it and write it into the boot cache.
|
||||
$tmpDir = storage_path('app/addons/ThrowawayTest'.uniqid());
|
||||
mkdir($tmpDir, 0755, true);
|
||||
file_put_contents($tmpDir.'/module.json', json_encode([
|
||||
'name' => 'ThrowawayTest',
|
||||
'alias' => 'throwawaytest',
|
||||
'providers' => [],
|
||||
]));
|
||||
|
||||
// Run AddonRuntimeService so the temp addon is discovered, upserted, and cached.
|
||||
app(AddonDiscoveryService::class)->run();
|
||||
|
||||
// Fetch the DB row AddonRuntimeService created for the temp addon.
|
||||
$throwaway = Addon::query()->where('path', $tmpDir)->firstOrFail();
|
||||
$throwawayId = $throwaway->id;
|
||||
|
||||
// Build a shim around it.
|
||||
$shim = new Module($throwaway, app(ManifestParser::class));
|
||||
|
||||
// Confirm it's in the cache before delete.
|
||||
$before = app(BootCache::class)->read();
|
||||
$pathsBefore = array_map(fn (AddonBootCache $r): string => $r->path, $before);
|
||||
expect($pathsBefore)->toContain($tmpDir);
|
||||
|
||||
// Delete the DB row (also removes the dir's module.json so re-prime won't re-add it).
|
||||
unlink($tmpDir.'/module.json');
|
||||
$shim->delete();
|
||||
|
||||
// DB row must be gone.
|
||||
expect(Addon::query()->where('id', $throwawayId)->first())->toBeNull();
|
||||
|
||||
// Boot cache must no longer list the deleted addon's path.
|
||||
$cached = app(BootCache::class)->read();
|
||||
$paths = array_map(fn (AddonBootCache $r): string => $r->path, $cached);
|
||||
expect($paths)->not->toContain($tmpDir);
|
||||
|
||||
// Cleanup temp dir.
|
||||
rmdir($tmpDir);
|
||||
});
|
||||
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Addons\Compat\Module;
|
||||
use App\Enums\AcarsType;
|
||||
use App\Enums\PirepFieldSource;
|
||||
use App\Enums\PirepState;
|
||||
@ -32,7 +31,6 @@ use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Nwidart\Modules\Facades\Module;
|
||||
|
||||
use function Pest\Laravel\seed;
|
||||
|
||||
@ -678,6 +676,8 @@ test('diversion handler', function (): void {
|
||||
Notification::assertSentTo([$pirep], PirepDiverted::class);
|
||||
});
|
||||
|
||||
/*
|
||||
// TODO: Move this into the vmsacars tests
|
||||
test('diversion handler reuses matching reposition flight and attaches subfleet', function (): void {
|
||||
updateSetting('pireps.handle_diversion', true);
|
||||
|
||||
@ -691,8 +691,6 @@ test('diversion handler reuses matching reposition flight and attaches subfleet'
|
||||
'value' => '1',
|
||||
]);
|
||||
|
||||
Module::shouldReceive('find')->andReturn(Mockery::mock(Module::class));
|
||||
|
||||
$pirepSvc = app(PirepService::class);
|
||||
|
||||
$airline = Airline::factory()->create();
|
||||
@ -759,4 +757,4 @@ test('diversion handler reuses matching reposition flight and attaches subfleet'
|
||||
|
||||
expect($matchingFlights)->toHaveCount(1)
|
||||
->and($repositionFlight->fresh()->subfleets->pluck('id')->all())->toBe([$subfleet->id]);
|
||||
});
|
||||
});*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user