diff --git a/src/Console/Commands/Make/MakeModule.php b/src/Console/Commands/Make/MakeModule.php index 997f4d6..ab3338a 100644 --- a/src/Console/Commands/Make/MakeModule.php +++ b/src/Console/Commands/Make/MakeModule.php @@ -110,7 +110,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/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 @@ +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..312898b --- /dev/null +++ b/src/Support/Autodiscovery/EventsPlugin.php @@ -0,0 +1,61 @@ +shouldDiscoverEvents()) { + return []; + } + + 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 + { + $data->each(function(array $listeners, string $event) { + foreach (array_unique($listeners, SORT_REGULAR) as $listener) { + $this->events->listen($event, $listener); + } + }); + } + + 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/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/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/Autodiscovery/Plugin.php b/src/Support/Autodiscovery/Plugin.php new file mode 100644 index 0000000..fd1528d --- /dev/null +++ b/src/Support/Autodiscovery/Plugin.php @@ -0,0 +1,13 @@ +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/RoutesPlugin.php b/src/Support/Autodiscovery/RoutesPlugin.php new file mode 100644 index 0000000..5de3ecc --- /dev/null +++ b/src/Support/Autodiscovery/RoutesPlugin.php @@ -0,0 +1,23 @@ +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/TranslatorPlugin.php b/src/Support/Autodiscovery/TranslatorPlugin.php new file mode 100644 index 0000000..8eebbb6 --- /dev/null +++ b/src/Support/Autodiscovery/TranslatorPlugin.php @@ -0,0 +1,38 @@ +langDirectoryFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $dir) => [ + 'namespace' => $dir->module()->name, + '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 new file mode 100644 index 0000000..a91fd2d --- /dev/null +++ b/src/Support/Autodiscovery/ViewPlugin.php @@ -0,0 +1,35 @@ +viewDirectoryFinder() + ->withModuleInfo() + ->values() + ->map(fn(ModuleFileInfo $dir) => [ + 'namespace' => $dir->module()->name, + 'path' => $dir->getRealPath(), + ]); + } + + public function handle(Collection $data) + { + $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 ccc9884..2816ff5 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -2,56 +2,36 @@ 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\BindingResolutionException; 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 Illuminate\Translation\Translator; -use Illuminate\View\Compilers\BladeCompiler; -use Illuminate\View\Factory as ViewFactory; -use Livewire\LivewireManager; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\AfterResolving; +use InterNACHI\Modular\Support\Autodiscovery\Attributes\OnBoot; +use InterNACHI\Modular\Support\Autodiscovery\Plugin; use ReflectionClass; use RuntimeException; -use Symfony\Component\Finder\SplFileInfo; use Throwable; class AutodiscoveryHelper { protected ?array $data = null; + protected array $plugins = []; + + protected array $handled = []; + public function __construct( protected FinderFactory $finders, protected Filesystem $fs, + protected Container $app, protected string $cache_path, ) { } - public function writeCache(Container $app): void + public function writeCache(): 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 (array_keys($this->plugins) as $plugin) { + $this->discover($plugin); } $cache = Collection::make($this->data)->toArray(); @@ -76,237 +56,80 @@ public function clearCache(): void if ($this->fs->exists($this->cache_path)) { $this->fs->delete($this->cache_path); } - } - - /** @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']))); + $this->handled = []; + $this->data = null; } - public function routes(): void + /** @param class-string $plugin */ + public function register(string $plugin): static { - $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']), - ); + $this->plugins[$plugin] ??= null; - // 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']); - }, - ); + return $this; } - public function migrations(Migrator $migrator): void + public function bootPlugins(): 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), - ); + 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($resolved) => $this->handle($class, $resolved)); + if ($this->app->resolved($abstract)) { + $this->handle($class); + } + return; + } + + if (OnBoot::class === $attribute->getName()) { + $this->handle($class); + return; + } + } + } } - public function commands(Artisan $artisan): void + /** @param class-string $name */ + public function discover(string $name): Collection { - $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), - ); + $this->data ??= $this->readData(); + $this->data[$name] ??= $this->plugin($name)->discover($this->finders); + + return collect($this->data[$name]); } - public function policies(Gate $gate): void + /** @param class-string $name */ + public function handle(string $name, ?object $dependency = null): mixed { - $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']), - ); + return $this->handled[$name] ??= $this->plugin($name, $dependency)->handle($this->discover($name)); } - public function events(Dispatcher $events, bool $autodiscover = true): void + public function handleIf(string $name, bool $condition): mixed { - $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); - } - }, - ); + if ($condition) { + return $this->handle($name); + } + + return null; } - public function livewire(LivewireManager $livewire): void + /** + * @template TPlugin of Plugin + * @param class-string $plugin + * @return TPlugin + */ + public function plugin(string $plugin, ?object $dependency = null): Plugin { - $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']), - ); - } - - protected function withCache( - string $key, - Closure $default, - ?Closure $each = null, - ): iterable { - $this->data ??= $this->readData(); - $this->data[$key] ??= value($default); + if (! isset($this->plugins[$plugin]) && $dependency) { + $this->app + ->when($plugin) + ->needs($dependency::class) + ->give(fn() => $dependency); + } - return $each - ? Collection::make($this->data[$key])->each($each) - : $this->data[$key]; + return $this->plugins[$plugin] ??= $this->app->make($plugin); } protected function readData(): array @@ -319,10 +142,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 34341ab..81ae004 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -3,33 +3,31 @@ 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\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\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\TranslatorPlugin; +use InterNACHI\Modular\Support\Autodiscovery\ViewPlugin; use Livewire\LivewireManager; class ModularServiceProvider extends ServiceProvider { - protected ?ModuleRegistry $registry = null; - protected ?AutodiscoveryHelper $autodiscovery_helper = null; protected string $base_dir; @@ -62,6 +60,7 @@ public function register(): void return new AutodiscoveryHelper( $app->make(FinderFactory::class), $app->make(Filesystem::class), + $app, $this->app->bootstrapPath('cache/app-modules.php') ); }); @@ -72,47 +71,25 @@ 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)); + PluginRegistry::register( + RoutesPlugin::class, + TranslatorPlugin::class, + ViewPlugin::class, + BladePlugin::class, + EventsPlugin::class, + MigratorPlugin::class, + GatePlugin::class, + ); - Artisan::starting(function(Artisan $artisan) { - $this->autodiscover()->commands($artisan); - $this->registerNamespacesInTinker(); - }); + $this->app->booting($this->bootPlugins(...)); } public function boot(): void - { - $this->publishVendorFiles(); - $this->bootPackageCommands(); - - $this->bootRoutes(); - $this->bootViews(); - $this->bootBladeComponents(); - $this->bootTranslations(); - $this->bootEvents(); - $this->bootLivewireComponents(); - } - - 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, @@ -124,74 +101,37 @@ protected function bootPackageCommands(): void } } - protected function bootRoutes(): void + protected function bootPlugins(): void { - if (! $this->app->routesAreCached()) { - $this->autodiscover()->routes(); + $plugins = PluginRegistry::instance()->all(); + + // First register all plugins with the auto-discovery helper + foreach ($plugins as $class) { + $this->autodiscover()->register($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($artisan) => $this->autodiscover()->handle(ArtisanPlugin::class, $artisan)); } - 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); - } - }); - } - - protected function bootEvents(): void - { - $this->callAfterResolving(Dispatcher::class, function(Dispatcher $events) { - $this->autodiscover()->events($events, $this->shouldDiscoverEvents()); - }); - } - - protected function bootLivewireComponents(): void + protected function autodiscover(): AutodiscoveryHelper { - if (class_exists(LivewireManager::class)) { - $this->autodiscover()->livewire($this->app->make(LivewireManager::class)); - } + 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()); } - 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) { @@ -201,18 +141,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()); - } } 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