From c214777cd32e82e4b9e32a664ac1c8f7bc014962 Mon Sep 17 00:00:00 2001 From: Bogdan Kharchenko Date: Fri, 26 Sep 2025 17:12:09 -0400 Subject: [PATCH 1/7] Introduce Auto-discovery Plugins --- src/Support/Autodiscovery/BladePlugin.php | 38 +++++ src/Support/Autodiscovery/EventsPlugin.php | 70 +++++++++ src/Support/Autodiscovery/Plugin.php | 14 ++ src/Support/Autodiscovery/RoutesPlugin.php | 23 +++ .../Autodiscovery/TranslationsPlugin.php | 45 ++++++ src/Support/Autodiscovery/ViewPlugin.php | 38 +++++ src/Support/AutodiscoveryHelper.php | 142 ++++++------------ src/Support/ModularServiceProvider.php | 77 ++++------ 8 files changed, 302 insertions(+), 145 deletions(-) create mode 100644 src/Support/Autodiscovery/BladePlugin.php create mode 100644 src/Support/Autodiscovery/EventsPlugin.php create mode 100644 src/Support/Autodiscovery/Plugin.php create mode 100644 src/Support/Autodiscovery/RoutesPlugin.php create mode 100644 src/Support/Autodiscovery/TranslationsPlugin.php create mode 100644 src/Support/Autodiscovery/ViewPlugin.php diff --git a/src/Support/Autodiscovery/BladePlugin.php b/src/Support/Autodiscovery/BladePlugin.php new file mode 100644 index 0000000..c09c75d --- /dev/null +++ b/src/Support/Autodiscovery/BladePlugin.php @@ -0,0 +1,38 @@ +blade = $blade; + + app(AutodiscoveryHelper::class)->handle(static::class); + } + + public function discover(FinderFactory $finders): iterable + { + return $finders + ->bladeComponentFileFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $component) => [ + 'prefix' => $component->module()->name, + 'fqcn' => $component->fullyQualifiedClassName(), + ]); + } + + public function handle(Collection $data) + { + $data->each(fn(array $row) => $this->blade->component($row['fqcn'], null, $row['prefix'])); + } +} diff --git a/src/Support/Autodiscovery/EventsPlugin.php b/src/Support/Autodiscovery/EventsPlugin.php new file mode 100644 index 0000000..3c46579 --- /dev/null +++ b/src/Support/Autodiscovery/EventsPlugin.php @@ -0,0 +1,70 @@ +app = $app; + $this->events = $events; + $this->config = $config; + + app(AutodiscoveryHelper::class)->handle(static::class); + } + + public function handle(Collection $data): void + { + $data->each(function(array $listeners, string $event) { + foreach (array_unique($listeners, SORT_REGULAR) as $listener) { + $this->events->listen($event, $listener); + } + }); + } + + public function discover(FinderFactory $finders): array + { + if (! $this->shouldDiscoverEvents()) { + return []; + } + + return $finders + ->listenerDirectoryFinder() + ->withModuleInfo() + ->reduce(function(array $discovered, ModuleFileInfo $file) { + return array_merge_recursive( + $discovered, + DiscoverEvents::within($file->getPathname(), $file->module()->path('src')) + ); + }, []); + } + + protected function shouldDiscoverEvents(): bool + { + return $this->config->get('app-modules.should_discover_events') ?? $this->appIsConfiguredToDiscoverEvents(); + } + + protected function appIsConfiguredToDiscoverEvents(): bool + { + return collect($this->app->getProviders(EventServiceProvider::class)) + ->filter(fn(EventServiceProvider $provider) => $provider::class === EventServiceProvider::class + || str_starts_with(get_class($provider), $this->app->getNamespace())) + ->contains(fn(EventServiceProvider $provider) => $provider->shouldDiscoverEvents()); + } +} diff --git a/src/Support/Autodiscovery/Plugin.php b/src/Support/Autodiscovery/Plugin.php new file mode 100644 index 0000000..ce48cb8 --- /dev/null +++ b/src/Support/Autodiscovery/Plugin.php @@ -0,0 +1,14 @@ +routeFileFinder() + ->values() + ->map(fn(SplFileInfo $file) => $file->getRealPath()); + } + + public function handle(Collection $data): void + { + $data->each(fn(string $filename) => require $filename); + } +} diff --git a/src/Support/Autodiscovery/TranslationsPlugin.php b/src/Support/Autodiscovery/TranslationsPlugin.php new file mode 100644 index 0000000..554496c --- /dev/null +++ b/src/Support/Autodiscovery/TranslationsPlugin.php @@ -0,0 +1,45 @@ +translator = $translator; + + app(AutodiscoveryHelper::class)->handle(static::class); + } + } + + public function handle(Collection $data): void + { + $data->each(function(array $row) { + $this->translator->addNamespace($row['namespace'], $row['path']); + $this->translator->addJsonPath($row['path']); + }); + } + + public function discover(FinderFactory $finders): array + { + return $finders + ->langDirectoryFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $dir) => [ + 'namespace' => $dir->module()->name, + 'path' => $dir->getRealPath(), + ]); + } +} diff --git a/src/Support/Autodiscovery/ViewPlugin.php b/src/Support/Autodiscovery/ViewPlugin.php new file mode 100644 index 0000000..18581c4 --- /dev/null +++ b/src/Support/Autodiscovery/ViewPlugin.php @@ -0,0 +1,38 @@ +factory = $factory; + + app(AutodiscoveryHelper::class)->handle(static::class); + } + + public function discover(FinderFactory $finders): iterable + { + return $finders + ->viewDirectoryFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $dir) => [ + 'namespace' => $dir->module()->name, + 'path' => $dir->getRealPath(), + ]); + } + + public function handle(Collection $data) + { + $data->each(fn(array $row) => $this->factory->addNamespace($row['namespace'], $row['path'])); + } +} diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index ccc9884..16fa531 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -6,7 +6,6 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Console\Command; use Illuminate\Contracts\Auth\Access\Gate; -use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Migrations\Migrator; @@ -16,6 +15,7 @@ use Illuminate\Translation\Translator; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Factory as ViewFactory; +use InterNACHI\Modular\Support\Autodiscovery\Plugin; use Livewire\LivewireManager; use ReflectionClass; use RuntimeException; @@ -26,32 +26,20 @@ class AutodiscoveryHelper { protected ?array $data = null; + protected array $plugins = []; + public function __construct( protected FinderFactory $finders, protected Filesystem $fs, + protected Container $app, protected string $cache_path, ) { } public function writeCache(Container $app): void { - $helpers = [ - $this->modules(...), - $this->routes(...), - $this->views(...), - $this->blade(...), - $this->translations(...), - $this->migrations(...), - $this->commands(...), - $this->policies(...), - $this->livewire(...), - ]; - - foreach ($helpers as $helper) { - try { - $app->call($helper); - } catch (BindingResolutionException) { - } + foreach ($this->plugins as $plugin) { + $this->discover($plugin); } $cache = Collection::make($this->data)->toArray(); @@ -78,6 +66,36 @@ public function clearCache(): void } } + /** @param class-string $plugin */ + public function register(string $plugin): static + { + $this->plugins[$plugin] ??= null; + + return $this; + } + + /** @param class-string $plugin */ + public function boot(string $plugin): Closure + { + return fn(...$args) => $this->plugin($plugin)->boot(...$args); + } + + /** @param class-string $name */ + public function discover(string $name): Collection + { + $this->data ??= $this->readData(); + $this->data[$name] ??= $this->plugin($name)->discover($this->finders); + + return collect($this->data[$name]); + } + + /** @param class-string $name */ + public function handle(string $name): mixed + { + return $this->plugin($name) + ->handle($this->discover($name)); + } + /** @return Collection */ public function modules(bool $reload = false): Collection { @@ -111,84 +129,6 @@ public function modules(bool $reload = false): Collection ->map(fn(array $d) => new ModuleConfig($d['name'], $d['base_path'], new Collection($d['namespaces']))); } - public function routes(): void - { - $this->withCache( - key: 'route_files', - default: fn() => $this->finders - ->routeFileFinder() - ->values() - ->map(fn(SplFileInfo $file) => $file->getRealPath()), - each: fn(string $filename) => require $filename - ); - } - - public function views(ViewFactory $factory): void - { - $this->withCache( - key: 'view_namespaces', - default: fn() => $this->finders - ->viewDirectoryFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $dir) => [ - 'namespace' => $dir->module()->name, - 'path' => $dir->getRealPath(), - ]), - each: fn(array $row) => $factory->addNamespace($row['namespace'], $row['path']), - ); - } - - public function blade(BladeCompiler $blade): void - { - // Handle individual Blade components (old syntax: ``) - $this->withCache( - key: 'blade_component_files', - default: fn() => $this->finders - ->bladeComponentFileFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $component) => [ - 'prefix' => $component->module()->name, - 'fqcn' => $component->fullyQualifiedClassName(), - ]), - each: fn(array $row) => $blade->component($row['fqcn'], null, $row['prefix']), - ); - - // Handle Blade component namespaces (new syntax: ``) - $this->withCache( - key: 'blade_component_dirs', - default: fn() => $this->finders - ->bladeComponentDirectoryFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $component) => [ - 'prefix' => $component->module()->name, - 'namespace' => $component->module()->qualify('View\\Components'), - ]), - each: fn(array $row) => $blade->componentNamespace($row['namespace'], $row['prefix']), - ); - } - - public function translations(Translator $translator): void - { - $this->withCache( - key: 'translation_files', - default: fn() => $this->finders - ->langDirectoryFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $dir) => [ - 'namespace' => $dir->module()->name, - 'path' => $dir->getRealPath(), - ]), - each: function(array $row) use ($translator) { - $translator->addNamespace($row['namespace'], $row['path']); - $translator->addJsonPath($row['path']); - }, - ); - } - public function migrations(Migrator $migrator): void { $this->withCache( @@ -296,6 +236,16 @@ public function livewire(LivewireManager $livewire): void ); } + /** + * @template TPlugin of Plugin + * @param class-string $plugin + * @return TPlugin + */ + public function plugin(string $plugin): Plugin + { + return $this->plugins[$plugin] ??= $this->app->make($plugin); + } + protected function withCache( string $key, Closure $default, diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 34341ab..141e2a8 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -6,24 +6,25 @@ use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Contracts\Translation\Translator as TranslatorContract; use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Eloquent\Factories\Factory as EloquentFactory; use Illuminate\Database\Migrations\Migrator; use Illuminate\Filesystem\Filesystem; -use Illuminate\Foundation\Support\Providers\EventServiceProvider; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; -use Illuminate\Translation\Translator; use Illuminate\View\Compilers\BladeCompiler; -use Illuminate\View\Factory as ViewFactory; use InterNACHI\Modular\Console\Commands\Make\MakeMigration; use InterNACHI\Modular\Console\Commands\Make\MakeModule; use InterNACHI\Modular\Console\Commands\ModulesCache; use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Console\Commands\ModulesList; use InterNACHI\Modular\Console\Commands\ModulesSync; +use InterNACHI\Modular\Support\Autodiscovery\BladePlugin; +use InterNACHI\Modular\Support\Autodiscovery\EventsPlugin; +use InterNACHI\Modular\Support\Autodiscovery\RoutesPlugin; +use InterNACHI\Modular\Support\Autodiscovery\TranslationsPlugin; +use InterNACHI\Modular\Support\Autodiscovery\ViewPlugin; use Livewire\LivewireManager; class ModularServiceProvider extends ServiceProvider @@ -62,6 +63,7 @@ public function register(): void return new AutodiscoveryHelper( $app->make(FinderFactory::class), $app->make(Filesystem::class), + $app, $this->app->bootstrapPath('cache/app-modules.php') ); }); @@ -86,10 +88,16 @@ public function boot(): void $this->publishVendorFiles(); $this->bootPackageCommands(); - $this->bootRoutes(); - $this->bootViews(); - $this->bootBladeComponents(); - $this->bootTranslations(); + $this->bootPlugins(); + + if (! $this->app->routesAreCached()) { + $this->autodiscover()->handle(RoutesPlugin::class); + } + + $this->callAfterResolving('view', $this->autodiscover()->boot(ViewPlugin::class)); + $this->callAfterResolving(BladeCompiler::class, $this->autodiscover()->boot(BladeCompiler::class)); + $this->callAfterResolving('translator', $this->autodiscover()->boot(TranslationsPlugin::class)); + $this->bootEvents(); $this->bootLivewireComponents(); } @@ -124,40 +132,24 @@ protected function bootPackageCommands(): void } } - protected function bootRoutes(): void + protected function bootPlugins() { - if (! $this->app->routesAreCached()) { - $this->autodiscover()->routes(); - } - } - - protected function bootViews(): void - { - $this->callAfterResolving('view', function(ViewFactory $factory) { - $this->autodiscover()->views($factory); - }); - } - - protected function bootBladeComponents(): void - { - $this->callAfterResolving(BladeCompiler::class, function(BladeCompiler $blade) { - $this->autodiscover()->blade($blade); - }); - } - - protected function bootTranslations(): void - { - $this->callAfterResolving('translator', function(TranslatorContract $translator) { - if ($translator instanceof Translator) { - $this->autodiscover()->translations($translator); - } - }); + $this->autodiscover() + ->register(RoutesPlugin::class) + ->register(TranslationsPlugin::class) + ->register(ViewPlugin::class) + ->register(BladePlugin::class) + ->register(EventsPlugin::class); } protected function bootEvents(): void { $this->callAfterResolving(Dispatcher::class, function(Dispatcher $events) { - $this->autodiscover()->events($events, $this->shouldDiscoverEvents()); + $this->autodiscover()->plugin(EventsPlugin::class)->boot( + app: $this->app, + events: $events, + config: $this->app->make('config'), + ); }); } @@ -202,17 +194,4 @@ protected function getModulesBasePath(): string return $this->modules_path; } - protected function shouldDiscoverEvents(): bool - { - return $this->app->make('config') - ->get('app-modules.should_discover_events') ?? $this->appIsConfiguredToDiscoverEvents(); - } - - protected function appIsConfiguredToDiscoverEvents(): bool - { - return collect($this->app->getProviders(EventServiceProvider::class)) - ->filter(fn(EventServiceProvider $provider) => $provider::class === EventServiceProvider::class - || str_starts_with(get_class($provider), $this->app->getNamespace())) - ->contains(fn(EventServiceProvider $provider) => $provider->shouldDiscoverEvents()); - } } From 63b0eddb1786a83f668156d115b9fb67e3149fd4 Mon Sep 17 00:00:00 2001 From: Bogdan Kharchenko Date: Sat, 27 Sep 2025 12:06:57 -0400 Subject: [PATCH 2/7] Tweaks --- src/Support/Autodiscovery/TranslationsPlugin.php | 6 +++--- src/Support/ModularServiceProvider.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Support/Autodiscovery/TranslationsPlugin.php b/src/Support/Autodiscovery/TranslationsPlugin.php index 554496c..1f4a621 100644 --- a/src/Support/Autodiscovery/TranslationsPlugin.php +++ b/src/Support/Autodiscovery/TranslationsPlugin.php @@ -14,11 +14,11 @@ class TranslationsPlugin extends Plugin { protected Translator $translator; - public function boot(BladeCompiler $blade) + public function boot(TranslatorContract $translator) { if ($translator instanceof Translator) { $this->translator = $translator; - + app(AutodiscoveryHelper::class)->handle(static::class); } } @@ -31,7 +31,7 @@ public function handle(Collection $data): void }); } - public function discover(FinderFactory $finders): array + public function discover(FinderFactory $finders): iterable { return $finders ->langDirectoryFinder() diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 141e2a8..acde8a1 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -95,7 +95,7 @@ public function boot(): void } $this->callAfterResolving('view', $this->autodiscover()->boot(ViewPlugin::class)); - $this->callAfterResolving(BladeCompiler::class, $this->autodiscover()->boot(BladeCompiler::class)); + $this->callAfterResolving(BladeCompiler::class, $this->autodiscover()->boot(BladePlugin::class)); $this->callAfterResolving('translator', $this->autodiscover()->boot(TranslationsPlugin::class)); $this->bootEvents(); From d985cda74e0b142c9da9d32867219631a8893433 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 3 Oct 2025 15:45:42 -0400 Subject: [PATCH 3/7] Refactor plugin architecture Co-Authored-By: Bogdan Kharchenko <32746389+bogdankharchenko@users.noreply.github.com> --- src/Support/Autodiscovery/ArtisanPlugin.php | 62 ++++++++++++ .../Attributes/AfterResolving.php | 14 +++ .../Autodiscovery/Attributes/OnBoot.php | 10 ++ src/Support/Autodiscovery/BladePlugin.php | 13 +-- src/Support/Autodiscovery/EventsPlugin.php | 47 ++++----- src/Support/Autodiscovery/GatePlugin.php | 53 ++++++++++ src/Support/Autodiscovery/LivewirePlugin.php | 43 ++++++++ src/Support/Autodiscovery/MigratorPlugin.php | 31 ++++++ src/Support/Autodiscovery/Plugin.php | 1 - src/Support/Autodiscovery/PluginRegistry.php | 43 ++++++++ ...lationsPlugin.php => TranslatorPlugin.php} | 35 +++---- src/Support/Autodiscovery/ViewPlugin.php | 15 ++- src/Support/AutodiscoveryHelper.php | 84 ++++++---------- src/Support/ModularServiceProvider.php | 97 ++++++------------- 14 files changed, 360 insertions(+), 188 deletions(-) create mode 100644 src/Support/Autodiscovery/ArtisanPlugin.php create mode 100644 src/Support/Autodiscovery/Attributes/AfterResolving.php create mode 100644 src/Support/Autodiscovery/Attributes/OnBoot.php create mode 100644 src/Support/Autodiscovery/GatePlugin.php create mode 100644 src/Support/Autodiscovery/LivewirePlugin.php create mode 100644 src/Support/Autodiscovery/MigratorPlugin.php create mode 100644 src/Support/Autodiscovery/PluginRegistry.php rename src/Support/Autodiscovery/{TranslationsPlugin.php => TranslatorPlugin.php} (62%) diff --git a/src/Support/Autodiscovery/ArtisanPlugin.php b/src/Support/Autodiscovery/ArtisanPlugin.php new file mode 100644 index 0000000..84dcaf6 --- /dev/null +++ b/src/Support/Autodiscovery/ArtisanPlugin.php @@ -0,0 +1,62 @@ +commandFileFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $file) => $file->fullyQualifiedClassName()) + ->filter($this->isInstantiableCommand(...)); + } + + public function handle(Collection $data): void + { + $data->each(fn(string $fqcn) => $this->artisan->resolve($fqcn)); + + $this->registerNamespacesInTinker(); + } + + protected function registerNamespacesInTinker(): void + { + if (! class_exists('Laravel\\Tinker\\TinkerServiceProvider')) { + return; + } + + $namespaces = $this->registry + ->modules() + ->flatMap(fn(ModuleConfig $config) => $config->namespaces) + ->reject(fn($ns) => Str::endsWith($ns, ['Tests\\', 'Database\\Factories\\', 'Database\\Seeders\\'])) + ->values() + ->all(); + + Config::set('tinker.alias', array_merge($namespaces, Config::get('tinker.alias', []))); + } + + protected function isInstantiableCommand($command): bool + { + return is_subclass_of($command, Command::class) + && ! (new ReflectionClass($command))->isAbstract(); + } +} diff --git a/src/Support/Autodiscovery/Attributes/AfterResolving.php b/src/Support/Autodiscovery/Attributes/AfterResolving.php new file mode 100644 index 0000000..3b22306 --- /dev/null +++ b/src/Support/Autodiscovery/Attributes/AfterResolving.php @@ -0,0 +1,14 @@ +blade = $blade; - - app(AutodiscoveryHelper::class)->handle(static::class); + public function __construct( + protected BladeCompiler $blade + ) { } public function discover(FinderFactory $finders): iterable diff --git a/src/Support/Autodiscovery/EventsPlugin.php b/src/Support/Autodiscovery/EventsPlugin.php index 3c46579..312898b 100644 --- a/src/Support/Autodiscovery/EventsPlugin.php +++ b/src/Support/Autodiscovery/EventsPlugin.php @@ -7,26 +7,34 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Support\Providers\EventServiceProvider; use Illuminate\Support\Collection; -use InterNACHI\Modular\Support\AutodiscoveryHelper; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; use InterNACHI\Modular\Support\DiscoverEvents; use InterNACHI\Modular\Support\FinderFactory; use InterNACHI\Modular\Support\ModuleFileInfo; +#[AfterResolving(Dispatcher::class)] class EventsPlugin extends Plugin { - protected Application $app; - - protected Dispatcher $events; - - protected Repository $config; + public function __construct( + protected Application $app, + protected Dispatcher $events, + protected Repository $config, + ) { + } - public function boot(Application $app, Dispatcher $events, Repository $config) + public function discover(FinderFactory $finders): array { - $this->app = $app; - $this->events = $events; - $this->config = $config; + if (! $this->shouldDiscoverEvents()) { + return []; + } - app(AutodiscoveryHelper::class)->handle(static::class); + return $finders + ->listenerDirectoryFinder() + ->withModuleInfo() + ->reduce(fn(array $discovered, ModuleFileInfo $file) => array_merge_recursive( + $discovered, + DiscoverEvents::within($file->getPathname(), $file->module()->path('src')) + ), []); } public function handle(Collection $data): void @@ -38,23 +46,6 @@ public function handle(Collection $data): void }); } - public function discover(FinderFactory $finders): array - { - if (! $this->shouldDiscoverEvents()) { - return []; - } - - return $finders - ->listenerDirectoryFinder() - ->withModuleInfo() - ->reduce(function(array $discovered, ModuleFileInfo $file) { - return array_merge_recursive( - $discovered, - DiscoverEvents::within($file->getPathname(), $file->module()->path('src')) - ); - }, []); - } - protected function shouldDiscoverEvents(): bool { return $this->config->get('app-modules.should_discover_events') ?? $this->appIsConfiguredToDiscoverEvents(); diff --git a/src/Support/Autodiscovery/GatePlugin.php b/src/Support/Autodiscovery/GatePlugin.php new file mode 100644 index 0000000..a50bba4 --- /dev/null +++ b/src/Support/Autodiscovery/GatePlugin.php @@ -0,0 +1,53 @@ +modelFileFinder() + ->withModuleInfo() + ->values() + ->map(function(ModuleFileInfo $file) { + $fqcn = $file->fullyQualifiedClassName(); + $namespace = rtrim($file->module()->namespaces->first(), '\\'); + + $candidates = [ + $namespace.'\\Policies\\'.Str::after($fqcn, 'Models\\').'Policy', // Policies/Foo/BarPolicy + $namespace.'\\Policies\\'.Str::afterLast($fqcn, '\\').'Policy', // Policies/BarPolicy + ]; + + foreach ($candidates as $candidate) { + if (class_exists($candidate)) { + return [ + 'fqcn' => $fqcn, + 'policy' => $candidate, + ]; + } + } + + return null; + }) + ->filter(); + } + + public function handle(Collection $data): void + { + $data->each(fn(array $row) => $this->gate->policy($row['fqcn'], $row['policy'])); + } +} diff --git a/src/Support/Autodiscovery/LivewirePlugin.php b/src/Support/Autodiscovery/LivewirePlugin.php new file mode 100644 index 0000000..89d4f59 --- /dev/null +++ b/src/Support/Autodiscovery/LivewirePlugin.php @@ -0,0 +1,43 @@ +livewireComponentFileFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $file) => [ + 'name' => sprintf( + '%s::%s', + $file->module()->name, + Str::of($file->getRelativePath()) + ->explode('/') + ->filter() + ->push($file->getBasename('.php')) + ->map([Str::class, 'kebab']) + ->implode('.') + ), + 'fqcn' => $file->fullyQualifiedClassName(), + ]); + } + + public function handle(Collection $data): void + { + $data->each(fn(array $d) => $this->livewire->component($d['name'], $d['fqcn'])); + } +} diff --git a/src/Support/Autodiscovery/MigratorPlugin.php b/src/Support/Autodiscovery/MigratorPlugin.php new file mode 100644 index 0000000..3e221b2 --- /dev/null +++ b/src/Support/Autodiscovery/MigratorPlugin.php @@ -0,0 +1,31 @@ +migrationDirectoryFinder() + ->values() + ->map(fn(SplFileInfo $file) => $file->getRealPath()); + } + + public function handle(Collection $data): void + { + $data->each(fn(string $path) => $this->migrator->path($path)); + } +} diff --git a/src/Support/Autodiscovery/Plugin.php b/src/Support/Autodiscovery/Plugin.php index ce48cb8..fd1528d 100644 --- a/src/Support/Autodiscovery/Plugin.php +++ b/src/Support/Autodiscovery/Plugin.php @@ -3,7 +3,6 @@ namespace InterNACHI\Modular\Support\Autodiscovery; use Illuminate\Support\Collection; -use Illuminate\Support\Enumerable; use InterNACHI\Modular\Support\FinderFactory; abstract class Plugin diff --git a/src/Support/Autodiscovery/PluginRegistry.php b/src/Support/Autodiscovery/PluginRegistry.php new file mode 100644 index 0000000..837fca0 --- /dev/null +++ b/src/Support/Autodiscovery/PluginRegistry.php @@ -0,0 +1,43 @@ +has(static::class)) { + $container->instance(static::class, new self()); + } + + return $container->make(static::class); + } + + /** @param class-string<\InterNACHI\Modular\Support\Autodiscovery\Plugin> ...$class */ + public static function register(string ...$class): void + { + static::instance()->add(...$class); + } + + /** @param class-string<\InterNACHI\Modular\Support\Autodiscovery\Plugin> ...$class */ + public function add(string ...$class): static + { + foreach ($class as $fqcn) { + $this->plugins[] = $fqcn; + } + + return $this; + } + + /** @return class-string<\InterNACHI\Modular\Support\Autodiscovery\Plugin>[] */ + public function all(): array + { + return $this->plugins; + } +} diff --git a/src/Support/Autodiscovery/TranslationsPlugin.php b/src/Support/Autodiscovery/TranslatorPlugin.php similarity index 62% rename from src/Support/Autodiscovery/TranslationsPlugin.php rename to src/Support/Autodiscovery/TranslatorPlugin.php index 554496c..19c6599 100644 --- a/src/Support/Autodiscovery/TranslationsPlugin.php +++ b/src/Support/Autodiscovery/TranslatorPlugin.php @@ -2,33 +2,18 @@ namespace InterNACHI\Modular\Support\Autodiscovery; -use Illuminate\Contracts\Translation\Translator as TranslatorContract; use Illuminate\Support\Collection; use Illuminate\Translation\Translator; -use Illuminate\View\Compilers\BladeCompiler; -use InterNACHI\Modular\Support\AutodiscoveryHelper; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; use InterNACHI\Modular\Support\FinderFactory; use InterNACHI\Modular\Support\ModuleFileInfo; -class TranslationsPlugin extends Plugin +#[AfterResolving(Translator::class)] +class TranslatorPlugin extends Plugin { - protected Translator $translator; - - public function boot(BladeCompiler $blade) - { - if ($translator instanceof Translator) { - $this->translator = $translator; - - app(AutodiscoveryHelper::class)->handle(static::class); - } - } - - public function handle(Collection $data): void - { - $data->each(function(array $row) { - $this->translator->addNamespace($row['namespace'], $row['path']); - $this->translator->addJsonPath($row['path']); - }); + public function __construct( + protected Translator $translator, + ) { } public function discover(FinderFactory $finders): array @@ -42,4 +27,12 @@ public function discover(FinderFactory $finders): array 'path' => $dir->getRealPath(), ]); } + + public function handle(Collection $data): void + { + $data->each(function(array $row) { + $this->translator->addNamespace($row['namespace'], $row['path']); + $this->translator->addJsonPath($row['path']); + }); + } } diff --git a/src/Support/Autodiscovery/ViewPlugin.php b/src/Support/Autodiscovery/ViewPlugin.php index 18581c4..a91fd2d 100644 --- a/src/Support/Autodiscovery/ViewPlugin.php +++ b/src/Support/Autodiscovery/ViewPlugin.php @@ -4,19 +4,16 @@ use Illuminate\Support\Collection; use Illuminate\View\Factory as ViewFactory; -use InterNACHI\Modular\Support\AutodiscoveryHelper; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; use InterNACHI\Modular\Support\FinderFactory; use InterNACHI\Modular\Support\ModuleFileInfo; +#[AfterResolving(ViewFactory::class)] class ViewPlugin extends Plugin { - protected ViewFactory $factory; - - public function boot(ViewFactory $factory) - { - $this->factory = $factory; - - app(AutodiscoveryHelper::class)->handle(static::class); + public function __construct( + protected ViewFactory $factory, + ) { } public function discover(FinderFactory $finders): iterable @@ -33,6 +30,6 @@ public function discover(FinderFactory $finders): iterable public function handle(Collection $data) { - $data->each(fn(array $row) => $this->factory->addNamespace($row['namespace'], $row['path'])); + $data->each(fn(array $d) => $this->factory->addNamespace($d['namespace'], $d['path'])); } } diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 16fa531..870b004 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -12,9 +12,8 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; use Illuminate\Support\Str; -use Illuminate\Translation\Translator; -use Illuminate\View\Compilers\BladeCompiler; -use Illuminate\View\Factory as ViewFactory; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\OnBoot; use InterNACHI\Modular\Support\Autodiscovery\Plugin; use Livewire\LivewireManager; use ReflectionClass; @@ -74,10 +73,26 @@ public function register(string $plugin): static return $this; } - /** @param class-string $plugin */ - public function boot(string $plugin): Closure + public function bootPlugins(): void { - return fn(...$args) => $this->plugin($plugin)->boot(...$args); + foreach ($this->plugins as $class => $_) { + $attributes = (new ReflectionClass($class))->getAttributes(); + foreach ($attributes as $attribute) { + if (AfterResolving::class === $attribute->getName()) { + $abstract = $attribute->getArguments()[0]; + $this->app->afterResolving($abstract, fn() => $this->handle($class)); + if ($this->app->resolved($abstract)) { + $this->handle($class); + } + return; + } + + if (OnBoot::class === $attribute->getName()) { + $this->handle($class); + return; + } + } + } } /** @param class-string $name */ @@ -92,8 +107,16 @@ public function discover(string $name): Collection /** @param class-string $name */ public function handle(string $name): mixed { - return $this->plugin($name) - ->handle($this->discover($name)); + return $this->plugin($name)->handle($this->discover($name)); + } + + public function handleIf(string $name, bool $condition): mixed + { + if ($condition) { + return $this->handle($name); + } + + return null; } /** @return Collection */ @@ -129,18 +152,6 @@ public function modules(bool $reload = false): Collection ->map(fn(array $d) => new ModuleConfig($d['name'], $d['base_path'], new Collection($d['namespaces']))); } - public function migrations(Migrator $migrator): void - { - $this->withCache( - key: 'migration_files', - default: fn() => $this->finders - ->migrationDirectoryFinder() - ->values() - ->map(fn(SplFileInfo $file) => $file->getRealPath()), - each: fn(string $path) => $migrator->path($path), - ); - } - public function commands(Artisan $artisan): void { $this->withCache( @@ -155,39 +166,6 @@ public function commands(Artisan $artisan): void ); } - public function policies(Gate $gate): void - { - $this->withCache( - key: 'model_policy_files', - default: fn() => $this->finders - ->modelFileFinder() - ->withModuleInfo() - ->values() - ->map(function(ModuleFileInfo $file) use ($gate) { - $fqcn = $file->fullyQualifiedClassName(); - $namespace = rtrim($file->module()->namespaces->first(), '\\'); - - $candidates = [ - $namespace.'\\Policies\\'.Str::after($fqcn, 'Models\\').'Policy', // Policies/Foo/BarPolicy - $namespace.'\\Policies\\'.Str::afterLast($fqcn, '\\').'Policy', // Policies/BarPolicy - ]; - - foreach ($candidates as $candidate) { - if (class_exists($candidate)) { - return [ - 'fqcn' => $fqcn, - 'policy' => $candidate, - ]; - } - } - - return null; - }) - ->filter(), - each: fn(array $row) => $gate->policy($row['fqcn'], $row['policy']), - ); - } - public function events(Dispatcher $events, bool $autodiscover = true): void { $this->withCache( diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 141e2a8..f4235e8 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -3,27 +3,26 @@ namespace InterNACHI\Modular\Support; use Illuminate\Console\Application as Artisan; -use Illuminate\Contracts\Auth\Access\Gate; -use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Eloquent\Factories\Factory as EloquentFactory; -use Illuminate\Database\Migrations\Migrator; use Illuminate\Filesystem\Filesystem; -use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; -use Illuminate\Support\Str; -use Illuminate\View\Compilers\BladeCompiler; use InterNACHI\Modular\Console\Commands\Make\MakeMigration; use InterNACHI\Modular\Console\Commands\Make\MakeModule; use InterNACHI\Modular\Console\Commands\ModulesCache; use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Console\Commands\ModulesList; use InterNACHI\Modular\Console\Commands\ModulesSync; +use InterNACHI\Modular\Support\Autodiscovery\ArtisanPlugin; use InterNACHI\Modular\Support\Autodiscovery\BladePlugin; use InterNACHI\Modular\Support\Autodiscovery\EventsPlugin; +use InterNACHI\Modular\Support\Autodiscovery\GatePlugin; +use InterNACHI\Modular\Support\Autodiscovery\LivewirePlugin; +use InterNACHI\Modular\Support\Autodiscovery\MigratorPlugin; +use InterNACHI\Modular\Support\Autodiscovery\PluginRegistry; use InterNACHI\Modular\Support\Autodiscovery\RoutesPlugin; -use InterNACHI\Modular\Support\Autodiscovery\TranslationsPlugin; +use InterNACHI\Modular\Support\Autodiscovery\TranslatorPlugin; use InterNACHI\Modular\Support\Autodiscovery\ViewPlugin; use Livewire\LivewireManager; @@ -74,13 +73,27 @@ public function register(): void $this->registerEloquentFactories(); - $this->app->resolving(Migrator::class, fn(Migrator $migrator) => $this->autodiscover()->migrations($migrator)); - $this->app->resolving(Gate::class, fn(Gate $gate) => $this->autodiscover()->policies($gate)); - - Artisan::starting(function(Artisan $artisan) { - $this->autodiscover()->commands($artisan); - $this->registerNamespacesInTinker(); + PluginRegistry::register( + RoutesPlugin::class, + TranslatorPlugin::class, + ViewPlugin::class, + BladePlugin::class, + EventsPlugin::class, + MigratorPlugin::class, + GatePlugin::class, + ); + + $this->app->booting(function() { + $plugins = PluginRegistry::instance()->all(); + + foreach ($plugins as $class) { + $this->autodiscover()->plugin($class); + } + + $this->autodiscover()->bootPlugins(); }); + + Artisan::starting(fn() => $this->autodiscover()->handle(ArtisanPlugin::class)); } public function boot(): void @@ -88,18 +101,10 @@ public function boot(): void $this->publishVendorFiles(); $this->bootPackageCommands(); - $this->bootPlugins(); - - if (! $this->app->routesAreCached()) { - $this->autodiscover()->handle(RoutesPlugin::class); - } - - $this->callAfterResolving('view', $this->autodiscover()->boot(ViewPlugin::class)); - $this->callAfterResolving(BladeCompiler::class, $this->autodiscover()->boot(BladeCompiler::class)); - $this->callAfterResolving('translator', $this->autodiscover()->boot(TranslationsPlugin::class)); + $this->autodiscover()->handleIf(RoutesPlugin::class, condition: ! $this->app->routesAreCached()); + $this->autodiscover()->handleIf(LivewirePlugin::class, condition: class_exists(LivewireManager::class)); - $this->bootEvents(); - $this->bootLivewireComponents(); + $this->autodiscover()->bootPlugins(); } protected function registry(): ModuleRegistry @@ -132,34 +137,6 @@ protected function bootPackageCommands(): void } } - protected function bootPlugins() - { - $this->autodiscover() - ->register(RoutesPlugin::class) - ->register(TranslationsPlugin::class) - ->register(ViewPlugin::class) - ->register(BladePlugin::class) - ->register(EventsPlugin::class); - } - - protected function bootEvents(): void - { - $this->callAfterResolving(Dispatcher::class, function(Dispatcher $events) { - $this->autodiscover()->plugin(EventsPlugin::class)->boot( - app: $this->app, - events: $events, - config: $this->app->make('config'), - ); - }); - } - - protected function bootLivewireComponents(): void - { - if (class_exists(LivewireManager::class)) { - $this->autodiscover()->livewire($this->app->make(LivewireManager::class)); - } - } - protected function registerEloquentFactories(): void { $helper = new DatabaseFactoryHelper($this->registry()); @@ -168,22 +145,6 @@ protected function registerEloquentFactories(): void EloquentFactory::guessFactoryNamesUsing($helper->factoryNameResolver()); } - protected function registerNamespacesInTinker(): void - { - if (! class_exists('Laravel\\Tinker\\TinkerServiceProvider')) { - return; - } - - $namespaces = $this->registry() - ->modules() - ->flatMap(fn(ModuleConfig $config) => $config->namespaces) - ->reject(fn($ns) => Str::endsWith($ns, ['Tests\\', 'Database\\Factories\\', 'Database\\Seeders\\'])) - ->values() - ->all(); - - Config::set('tinker.alias', array_merge($namespaces, Config::get('tinker.alias', []))); - } - protected function getModulesBasePath(): string { if (null === $this->modules_path) { From 89bd725c49e4e36f66cb7debc8947fa7fa4082c9 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Fri, 3 Oct 2025 16:34:49 -0400 Subject: [PATCH 4/7] Move modules to plugin Co-Authored-By: Bogdan Kharchenko <32746389+bogdankharchenko@users.noreply.github.com> --- src/Console/Commands/Make/MakeModule.php | 4 +- src/Console/Commands/ModulesCache.php | 2 +- src/Support/Autodiscovery/ModulesPlugin.php | 39 ++++++ src/Support/AutodiscoveryHelper.php | 125 +------------------- src/Support/ModularServiceProvider.php | 67 +++++------ src/Support/ModuleRegistry.php | 15 ++- 6 files changed, 83 insertions(+), 169 deletions(-) create mode 100644 src/Support/Autodiscovery/ModulesPlugin.php diff --git a/src/Console/Commands/Make/MakeModule.php b/src/Console/Commands/Make/MakeModule.php index 997f4d6..c3ec93d 100644 --- a/src/Console/Commands/Make/MakeModule.php +++ b/src/Console/Commands/Make/MakeModule.php @@ -9,6 +9,8 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use InterNACHI\Modular\Console\Commands\ModulesClear; +use InterNACHI\Modular\Support\Autodiscovery\ModulesPlugin; +use InterNACHI\Modular\Support\AutodiscoveryHelper; use InterNACHI\Modular\Support\ModuleRegistry; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Terminal; @@ -110,7 +112,7 @@ public function handle() $this->line("Please run composer update {$this->composer_name}"); $this->newLine(); - $this->module_registry->reload(); + $this->getLaravel()->make(ModuleRegistry::class)->reload(); return 0; } diff --git a/src/Console/Commands/ModulesCache.php b/src/Console/Commands/ModulesCache.php index 47a93ab..0fbca74 100644 --- a/src/Console/Commands/ModulesCache.php +++ b/src/Console/Commands/ModulesCache.php @@ -15,7 +15,7 @@ public function handle(AutodiscoveryHelper $helper) { $this->call(ModulesClear::class); - $helper->writeCache($this->getLaravel()); + $helper->writeCache(); $this->info('Modules cached successfully!'); } diff --git a/src/Support/Autodiscovery/ModulesPlugin.php b/src/Support/Autodiscovery/ModulesPlugin.php new file mode 100644 index 0000000..44beb2e --- /dev/null +++ b/src/Support/Autodiscovery/ModulesPlugin.php @@ -0,0 +1,39 @@ +moduleComposerFileFinder() + ->values() + ->mapWithKeys(function(SplFileInfo $file) { + $composer_config = json_decode($file->getContents(), true, 16, JSON_THROW_ON_ERROR); + $base_path = rtrim(str_replace('\\', '/', $file->getPath()), '/'); + $name = basename($base_path); + + return [ + $name => [ + 'name' => $name, + 'base_path' => $base_path, + 'namespaces' => Collection::make($composer_config['autoload']['psr-4'] ?? []) + ->mapWithKeys(fn($src, $namespace) => ["{$base_path}/{$src}" => $namespace]) + ->all(), + ], + ]; + }); + } + + /** @return Collection */ + public function handle(Collection $data): Collection + { + return $data->map(fn(array $d) => new ModuleConfig($d['name'], $d['base_path'], new Collection($d['namespaces']))); + } +} diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 870b004..1e715c3 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -2,23 +2,14 @@ namespace InterNACHI\Modular\Support; -use Closure; -use Illuminate\Console\Application as Artisan; -use Illuminate\Console\Command; -use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Container\Container; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Database\Migrations\Migrator; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; use InterNACHI\Modular\Support\Autodiscovery\Attributes\OnBoot; use InterNACHI\Modular\Support\Autodiscovery\Plugin; -use Livewire\LivewireManager; use ReflectionClass; use RuntimeException; -use Symfony\Component\Finder\SplFileInfo; use Throwable; class AutodiscoveryHelper @@ -35,7 +26,7 @@ public function __construct( ) { } - public function writeCache(Container $app): void + public function writeCache(): void { foreach ($this->plugins as $plugin) { $this->discover($plugin); @@ -119,101 +110,6 @@ public function handleIf(string $name, bool $condition): mixed return null; } - /** @return Collection */ - public function modules(bool $reload = false): Collection - { - if ($reload) { - unset($this->data['modules']); - } - - $data = $this->withCache( - key: 'modules', - default: fn() => $this->finders - ->moduleComposerFileFinder() - ->values() - ->mapWithKeys(function(SplFileInfo $file) { - $composer_config = json_decode($file->getContents(), true, 16, JSON_THROW_ON_ERROR); - $base_path = rtrim(str_replace('\\', '/', $file->getPath()), '/'); - $name = basename($base_path); - - return [ - $name => [ - 'name' => $name, - 'base_path' => $base_path, - 'namespaces' => Collection::make($composer_config['autoload']['psr-4'] ?? []) - ->mapWithKeys(fn($src, $namespace) => ["{$base_path}/{$src}" => $namespace]) - ->all(), - ], - ]; - }), - ); - - return Collection::make($data) - ->map(fn(array $d) => new ModuleConfig($d['name'], $d['base_path'], new Collection($d['namespaces']))); - } - - public function commands(Artisan $artisan): void - { - $this->withCache( - key: 'command_files', - default: fn() => $this->finders - ->commandFileFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $file) => $file->fullyQualifiedClassName()) - ->filter($this->isInstantiableCommand(...)), - each: fn(string $fqcn) => $artisan->resolve($fqcn), - ); - } - - public function events(Dispatcher $events, bool $autodiscover = true): void - { - $this->withCache( - key: 'events', - default: fn() => $autodiscover - ? $this->finders - ->listenerDirectoryFinder() - ->withModuleInfo() - ->reduce(function(array $discovered, ModuleFileInfo $file) { - return array_merge_recursive( - $discovered, - DiscoverEvents::within($file->getPathname(), $file->module()->path('src')) - ); - }, []) - : [], - each: function(array $listeners, string $event) use ($events) { - foreach (array_unique($listeners, SORT_REGULAR) as $listener) { - $events->listen($event, $listener); - } - }, - ); - } - - public function livewire(LivewireManager $livewire): void - { - $this->withCache( - key: 'livewire_component_files', - default: fn() => $this->finders - ->livewireComponentFileFinder() - ->withModuleInfo() - ->values() - ->map(fn(ModuleFileInfo $file) => [ - 'name' => sprintf( - '%s::%s', - $file->module()->name, - Str::of($file->getRelativePath()) - ->explode('/') - ->filter() - ->push($file->getBasename('.php')) - ->map([Str::class, 'kebab']) - ->implode('.') - ), - 'fqcn' => $file->fullyQualifiedClassName(), - ]), - each: fn(array $row) => $livewire->component($row['name'], $row['fqcn']), - ); - } - /** * @template TPlugin of Plugin * @param class-string $plugin @@ -224,19 +120,6 @@ public function plugin(string $plugin): Plugin return $this->plugins[$plugin] ??= $this->app->make($plugin); } - protected function withCache( - string $key, - Closure $default, - ?Closure $each = null, - ): iterable { - $this->data ??= $this->readData(); - $this->data[$key] ??= value($default); - - return $each - ? Collection::make($this->data[$key])->each($each) - : $this->data[$key]; - } - protected function readData(): array { try { @@ -247,10 +130,4 @@ protected function readData(): array return []; } } - - protected function isInstantiableCommand($command): bool - { - return is_subclass_of($command, Command::class) - && ! (new ReflectionClass($command))->isAbstract(); - } } diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index f4235e8..ecf3a46 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Eloquent\Factories\Factory as EloquentFactory; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; use InterNACHI\Modular\Console\Commands\Make\MakeMigration; use InterNACHI\Modular\Console\Commands\Make\MakeModule; @@ -20,6 +21,7 @@ use InterNACHI\Modular\Support\Autodiscovery\GatePlugin; use InterNACHI\Modular\Support\Autodiscovery\LivewirePlugin; use InterNACHI\Modular\Support\Autodiscovery\MigratorPlugin; +use InterNACHI\Modular\Support\Autodiscovery\ModulesPlugin; use InterNACHI\Modular\Support\Autodiscovery\PluginRegistry; use InterNACHI\Modular\Support\Autodiscovery\RoutesPlugin; use InterNACHI\Modular\Support\Autodiscovery\TranslatorPlugin; @@ -28,8 +30,6 @@ class ModularServiceProvider extends ServiceProvider { - protected ?ModuleRegistry $registry = null; - protected ?AutodiscoveryHelper $autodiscovery_helper = null; protected string $base_dir; @@ -83,49 +83,15 @@ public function register(): void GatePlugin::class, ); - $this->app->booting(function() { - $plugins = PluginRegistry::instance()->all(); - - foreach ($plugins as $class) { - $this->autodiscover()->plugin($class); - } - - $this->autodiscover()->bootPlugins(); - }); - - Artisan::starting(fn() => $this->autodiscover()->handle(ArtisanPlugin::class)); + $this->app->booting($this->bootPlugins(...)); } public function boot(): void - { - $this->publishVendorFiles(); - $this->bootPackageCommands(); - - $this->autodiscover()->handleIf(RoutesPlugin::class, condition: ! $this->app->routesAreCached()); - $this->autodiscover()->handleIf(LivewirePlugin::class, condition: class_exists(LivewireManager::class)); - - $this->autodiscover()->bootPlugins(); - } - - protected function registry(): ModuleRegistry - { - return $this->registry ??= $this->app->make(ModuleRegistry::class); - } - - protected function autodiscover(): AutodiscoveryHelper - { - return $this->autodiscovery_helper ??= $this->app->make(AutodiscoveryHelper::class); - } - - protected function publishVendorFiles(): void { $this->publishes([ "{$this->base_dir}/config/app-modules.php" => $this->app->configPath('app-modules.php'), ], 'modular-config'); - } - - protected function bootPackageCommands(): void - { + if ($this->app->runningInConsole()) { $this->commands([ MakeModule::class, @@ -137,9 +103,32 @@ protected function bootPackageCommands(): void } } + protected function bootPlugins(): void + { + $plugins = PluginRegistry::instance()->all(); + + // First register all plugins with the auto-discovery helper + foreach ($plugins as $class) { + $this->autodiscover()->plugin($class); + } + + // Then boot all plugins that have annotations + $this->autodiscover()->bootPlugins(); + + // Finally, handle some special plugin cases + $this->autodiscover()->handleIf(RoutesPlugin::class, condition: ! $this->app->routesAreCached()); + $this->autodiscover()->handleIf(LivewirePlugin::class, condition: class_exists(LivewireManager::class)); + Artisan::starting(fn() => $this->autodiscover()->handle(ArtisanPlugin::class)); + } + + protected function autodiscover(): AutodiscoveryHelper + { + return $this->autodiscovery_helper ??= $this->app->make(AutodiscoveryHelper::class); + } + protected function registerEloquentFactories(): void { - $helper = new DatabaseFactoryHelper($this->registry()); + $helper = new DatabaseFactoryHelper($this->app->make(ModuleRegistry::class)); EloquentFactory::guessModelNamesUsing($helper->modelNameResolver()); EloquentFactory::guessFactoryNamesUsing($helper->factoryNameResolver()); diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index 528177d..fd31c11 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use InterNACHI\Modular\Exceptions\CannotFindModuleForPathException; +use InterNACHI\Modular\Support\Autodiscovery\ModulesPlugin; class ModuleRegistry { @@ -56,16 +57,22 @@ public function moduleForClass(string $fqcn): ?ModuleConfig }); } + public function setModules(Collection $modules): static + { + $this->modules = $modules->ensure(ModulesPlugin::class); + + return $this; + } + + /** @return Collection */ public function modules(): Collection { - return $this->modules ??= $this->autodiscovery_helper->modules(); + return $this->modules ??= $this->autodiscovery_helper->handle(ModulesPlugin::class); } public function reload(): Collection { - $this->modules = null; - - return $this->modules ??= $this->autodiscovery_helper->modules(reload: true); + return $this->modules = $this->autodiscovery_helper->handle(ModulesPlugin::class); } protected function extractModuleNameFromPath(string $path): string From cb91e56d8ea56ed648a90d5cefcb5694e515d98b Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Tue, 30 Dec 2025 14:43:11 -0500 Subject: [PATCH 5/7] Update plugin discovery to support dependency injection and prevent duplicate execution --- .../Autodiscovery/TranslatorPlugin.php | 2 +- src/Support/AutodiscoveryHelper.php | 21 ++++++++++++++----- src/Support/ModularServiceProvider.php | 6 ++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Support/Autodiscovery/TranslatorPlugin.php b/src/Support/Autodiscovery/TranslatorPlugin.php index 19c6599..8eebbb6 100644 --- a/src/Support/Autodiscovery/TranslatorPlugin.php +++ b/src/Support/Autodiscovery/TranslatorPlugin.php @@ -16,7 +16,7 @@ public function __construct( ) { } - public function discover(FinderFactory $finders): array + public function discover(FinderFactory $finders): iterable { return $finders ->langDirectoryFinder() diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 1e715c3..ebe2162 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -18,6 +18,8 @@ class AutodiscoveryHelper protected array $plugins = []; + protected array $handled = []; + public function __construct( protected FinderFactory $finders, protected Filesystem $fs, @@ -28,7 +30,7 @@ public function __construct( public function writeCache(): void { - foreach ($this->plugins as $plugin) { + foreach (array_keys($this->plugins) as $plugin) { $this->discover($plugin); } @@ -54,6 +56,8 @@ public function clearCache(): void if ($this->fs->exists($this->cache_path)) { $this->fs->delete($this->cache_path); } + + $this->data = null; } /** @param class-string $plugin */ @@ -71,7 +75,7 @@ public function bootPlugins(): void foreach ($attributes as $attribute) { if (AfterResolving::class === $attribute->getName()) { $abstract = $attribute->getArguments()[0]; - $this->app->afterResolving($abstract, fn() => $this->handle($class)); + $this->app->afterResolving($abstract, fn($resolved) => $this->handle($class, $resolved)); if ($this->app->resolved($abstract)) { $this->handle($class); } @@ -96,9 +100,9 @@ public function discover(string $name): Collection } /** @param class-string $name */ - public function handle(string $name): mixed + public function handle(string $name, ?object $dependency = null): mixed { - return $this->plugin($name)->handle($this->discover($name)); + return $this->handled[$name] ??= $this->plugin($name, $dependency)->handle($this->discover($name)); } public function handleIf(string $name, bool $condition): mixed @@ -115,8 +119,15 @@ public function handleIf(string $name, bool $condition): mixed * @param class-string $plugin * @return TPlugin */ - public function plugin(string $plugin): Plugin + public function plugin(string $plugin, ?object $dependency = null): Plugin { + if (! isset($this->plugins[$plugin]) && $dependency) { + $this->app + ->when($plugin) + ->needs($dependency::class) + ->give(fn() => $dependency); + } + return $this->plugins[$plugin] ??= $this->app->make($plugin); } diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index ecf3a46..1f24e3e 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -7,7 +7,6 @@ use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Eloquent\Factories\Factory as EloquentFactory; use Illuminate\Filesystem\Filesystem; -use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; use InterNACHI\Modular\Console\Commands\Make\MakeMigration; use InterNACHI\Modular\Console\Commands\Make\MakeModule; @@ -21,7 +20,6 @@ use InterNACHI\Modular\Support\Autodiscovery\GatePlugin; use InterNACHI\Modular\Support\Autodiscovery\LivewirePlugin; use InterNACHI\Modular\Support\Autodiscovery\MigratorPlugin; -use InterNACHI\Modular\Support\Autodiscovery\ModulesPlugin; use InterNACHI\Modular\Support\Autodiscovery\PluginRegistry; use InterNACHI\Modular\Support\Autodiscovery\RoutesPlugin; use InterNACHI\Modular\Support\Autodiscovery\TranslatorPlugin; @@ -109,7 +107,7 @@ protected function bootPlugins(): void // First register all plugins with the auto-discovery helper foreach ($plugins as $class) { - $this->autodiscover()->plugin($class); + $this->autodiscover()->register($class); } // Then boot all plugins that have annotations @@ -118,7 +116,7 @@ protected function bootPlugins(): void // Finally, handle some special plugin cases $this->autodiscover()->handleIf(RoutesPlugin::class, condition: ! $this->app->routesAreCached()); $this->autodiscover()->handleIf(LivewirePlugin::class, condition: class_exists(LivewireManager::class)); - Artisan::starting(fn() => $this->autodiscover()->handle(ArtisanPlugin::class)); + Artisan::starting(fn($artisan) => $this->autodiscover()->handle(ArtisanPlugin::class, $artisan)); } protected function autodiscover(): AutodiscoveryHelper From 2fee0fa10b1ba6939f9fd96430494273b80a0aaa Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Tue, 30 Dec 2025 15:08:25 -0500 Subject: [PATCH 6/7] Code style --- src/Console/Commands/Make/MakeModule.php | 2 -- src/Support/ModularServiceProvider.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Console/Commands/Make/MakeModule.php b/src/Console/Commands/Make/MakeModule.php index c3ec93d..ab3338a 100644 --- a/src/Console/Commands/Make/MakeModule.php +++ b/src/Console/Commands/Make/MakeModule.php @@ -9,8 +9,6 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use InterNACHI\Modular\Console\Commands\ModulesClear; -use InterNACHI\Modular\Support\Autodiscovery\ModulesPlugin; -use InterNACHI\Modular\Support\AutodiscoveryHelper; use InterNACHI\Modular\Support\ModuleRegistry; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Terminal; diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 1f24e3e..81ae004 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -141,5 +141,4 @@ protected function getModulesBasePath(): string return $this->modules_path; } - } From 68b4f86f70cb9f953108ac81aa9b685bddf6575f Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Tue, 30 Dec 2025 15:36:29 -0500 Subject: [PATCH 7/7] Reset handled array in clearCache to prevent stale state --- src/Support/AutodiscoveryHelper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index ebe2162..2816ff5 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -57,6 +57,7 @@ public function clearCache(): void $this->fs->delete($this->cache_path); } + $this->handled = []; $this->data = null; }