From 0faff28eb523727c5257213b03ac54ac9a7dc14f Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 13:49:37 -0400 Subject: [PATCH 01/16] wip --- composer.json | 8 +- src/Support/AutodiscoveryHelper.php | 213 ++++++++++++++++ src/Support/Cache.php | 40 +++ src/Support/FinderCollection.php | 12 +- ...oDiscoveryHelper.php => FinderFactory.php} | 10 +- src/Support/ModularEventServiceProvider.php | 2 +- src/Support/ModularServiceProvider.php | 227 ++++-------------- src/Support/ModuleFileInfo.php | 23 ++ tests/AutoDiscoveryHelperTest.php | 7 +- 9 files changed, 338 insertions(+), 204 deletions(-) create mode 100644 src/Support/AutodiscoveryHelper.php create mode 100644 src/Support/Cache.php rename src/Support/{AutoDiscoveryHelper.php => FinderFactory.php} (90%) create mode 100644 src/Support/ModuleFileInfo.php diff --git a/composer.json b/composer.json index 3776ca0..f5bc368 100644 --- a/composer.json +++ b/composer.json @@ -16,17 +16,17 @@ "type": "library", "license": "MIT", "require": { - "php": ">=8.0", + "php": ">=8.3", "ext-simplexml": "*", "ext-dom": "*", "composer/composer": "^2.1", - "illuminate/support": "^9|^10|^11|^12|13.x-dev|dev-master|dev-main" + "illuminate/support": "^11|^12|13.x-dev|dev-master|dev-main" }, "require-dev": { - "orchestra/testbench": "^7.52|^8.33|^9.11|^10.0|dev-master|dev-main", + "orchestra/testbench": "^9.11|^10.0|dev-master|dev-main", "friendsofphp/php-cs-fixer": "^3.14", "mockery/mockery": "^1.5", - "phpunit/phpunit": "^9.5|^10.5|^11.5", + "phpunit/phpunit": "^10.5|^11.5", "ext-json": "*", "livewire/livewire": "^2.5|^3.0" }, diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php new file mode 100644 index 0000000..7df9e3b --- /dev/null +++ b/src/Support/AutodiscoveryHelper.php @@ -0,0 +1,213 @@ +withCache( + key: 'route_files', + default: fn() => $this->finders + ->routeFileFinder() + ->map(fn(SplFileInfo $file) => $file->getRealPath()), + each: fn(string $filename) => require $filename + ); + } + + public function views(ViewFactory $factory): void + { + $this->withCache( + key: 'view_namespaces', + default: $this->finders + ->viewDirectoryFinder() + ->withModuleInfo() + ->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: $this->finders + ->bladeComponentFileFinder() + ->withModuleInfo() + ->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: $this->finders + ->bladeComponentDirectoryFinder() + ->withModuleInfo() + ->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: 'blade_component_files', + default: $this->finders + ->langDirectoryFinder() + ->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( + key: 'migration_files', + default: $this->finders + ->migrationDirectoryFinder() + ->map(fn(SplFileInfo $file) => $file->getRealPath()), + each: fn(string $path) => $migrator->path($path), + ); + } + + public function commands(Artisan $artisan): void + { + $this->withCache( + key: 'command_files', + default: $this->finders + ->commandFileFinder() + ->withModuleInfo() + ->map(fn(ModuleFileInfo $file) => $file->fullyQualifiedClassName()) + ->filter($this->isInstantiableCommand(...)), + each: fn(string $fqcn) => $artisan->resolve($fqcn), + ); + } + + public function policies(Gate $gate): void + { + $this->withCache( + key: 'model_policy_files', + default: $this->finders + ->modelFileFinder() + ->withModuleInfo() + ->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 livewire(LivewireManager $livewire): void + { + $this->withCache( + key: 'livewire_component_files', + default: $this->finders + ->livewireComponentFileFinder() + ->withModuleInfo() + ->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, + ): Enumerable|array { + $this->data ??= $this->readData(); + $this->data[$key] ??= value($default); + + return $each + ? collect($this->data[$key])->each($each) + : $this->data[$key]; + } + + protected function readData(): array + { + try { + return $this->filesystem->exists($this->cache_path) + ? require $this->cache_path + : []; + } catch (Throwable) { + return []; + } + } + + protected function isInstantiableCommand($command): bool + { + return is_subclass_of($command, Command::class) + && ! (new ReflectionClass($command))->isAbstract(); + } +} diff --git a/src/Support/Cache.php b/src/Support/Cache.php new file mode 100644 index 0000000..3d7301d --- /dev/null +++ b/src/Support/Cache.php @@ -0,0 +1,40 @@ +data($key, $callback)); + } + + public function save(): bool + { + $cache_contents = 'data, true).';'.PHP_EOL; + + return $this->filesystem->put($this->path, $cache_contents); + } + + protected function data(?string $key = null, mixed $default = null): array + { + if (null === $this->data && $this->filesystem->exists($this->path)) { + $this->data = require $this->path; + } + + return Arr::get($this->data, $key, $default); + } +} diff --git a/src/Support/FinderCollection.php b/src/Support/FinderCollection.php index a1db3c9..abee334 100644 --- a/src/Support/FinderCollection.php +++ b/src/Support/FinderCollection.php @@ -6,6 +6,7 @@ use Illuminate\Support\Traits\ForwardsCalls; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; /** * @mixin \Illuminate\Support\LazyCollection @@ -15,7 +16,7 @@ class FinderCollection { use ForwardsCalls; - protected const PREFER_COLLECTION_METHODS = ['filter', 'each', 'map']; + protected const array PREFER_COLLECTION_METHODS = ['filter', 'each', 'map']; public static function forFiles(): self { @@ -45,6 +46,15 @@ public function inOrEmpty(string|array $dirs): static } } + public function withModuleInfo(): static + { + return $this->map(fn(SplFileInfo $file) => new ModuleFileInfo( + $file->getFilename(), + $file->getRelativePath(), + $file->getRelativePathname(), + )); + } + public function __call($name, $arguments) { $result = $this->forwardCallTo($this->forwardCallTargetForMethod($name), $name, $arguments); diff --git a/src/Support/AutoDiscoveryHelper.php b/src/Support/FinderFactory.php similarity index 90% rename from src/Support/AutoDiscoveryHelper.php rename to src/Support/FinderFactory.php index a59996e..9270865 100644 --- a/src/Support/AutoDiscoveryHelper.php +++ b/src/Support/FinderFactory.php @@ -2,17 +2,11 @@ namespace InterNACHI\Modular\Support; -use Illuminate\Filesystem\Filesystem; - -class AutoDiscoveryHelper +class FinderFactory { - protected string $base_path; - public function __construct( - protected ModuleRegistry $module_registry, - protected Filesystem $filesystem + protected string $base_path ) { - $this->base_path = $module_registry->getModulesPath(); } public function commandFileFinder(): FinderCollection diff --git a/src/Support/ModularEventServiceProvider.php b/src/Support/ModularEventServiceProvider.php index e547af7..e7300f8 100644 --- a/src/Support/ModularEventServiceProvider.php +++ b/src/Support/ModularEventServiceProvider.php @@ -52,7 +52,7 @@ public function discoverEvents() { $modules = $this->app->make(ModuleRegistry::class); - return $this->app->make(AutoDiscoveryHelper::class) + return $this->app->make(FinderFactory::class) ->listenerDirectoryFinder() ->map(fn(SplFileInfo $directory) => $directory->getPathname()) ->reduce(function($discovered, string $directory) use ($modules) { diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 22120ca..b2aded0 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -2,10 +2,9 @@ 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\Filesystem\Filesystem; use Illuminate\Contracts\Translation\Translator as TranslatorContract; use Illuminate\Database\Console\Migrations\MigrateMakeCommand; use Illuminate\Database\Eloquent\Factories\Factory as EloquentFactory; @@ -22,15 +21,13 @@ use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Console\Commands\ModulesList; use InterNACHI\Modular\Console\Commands\ModulesSync; -use Livewire\Livewire; -use ReflectionClass; -use Symfony\Component\Finder\SplFileInfo; +use Livewire\LivewireManager; class ModularServiceProvider extends ServiceProvider { protected ?ModuleRegistry $registry = null; - protected ?AutoDiscoveryHelper $auto_discovery_helper = null; + protected ?AutodiscoveryHelper $autodiscovery_helper = null; protected string $base_dir; @@ -50,11 +47,21 @@ public function register(): void $this->app->singleton(ModuleRegistry::class, function() { return new ModuleRegistry( $this->getModulesBasePath(), - $this->app->bootstrapPath('cache/modules.php') + $this->app->bootstrapPath('cache/modules.php') // FIXME ); }); - $this->app->singleton(AutoDiscoveryHelper::class); + $this->app->singleton(FinderFactory::class, function() { + return new FinderFactory($this->getModulesBasePath()); + }); + + $this->app->singleton(AutodiscoveryHelper::class, function($app) { + return new AutodiscoveryHelper( + $app->make(FinderFactory::class), + $app->make(Filesystem::class), + $this->app->bootstrapPath('cache/app-modules.php') + ); + }); $this->app->singleton(MakeMigration::class, function($app) { return new MigrateMakeCommand($app['migration.creator'], $app['composer']); @@ -62,14 +69,13 @@ public function register(): void $this->registerEloquentFactories(); - // Set up lazy registrations for things that only need to run if we're using - // that functionality (e.g. we only need to look for and register migrations - // if we're running the migrator) - $this->registerLazily(Migrator::class, [$this, 'registerMigrations']); - $this->registerLazily(Gate::class, [$this, 'registerPolicies']); + $this->app->resolving(Migrator::class, fn(Migrator $migrator) => $this->autodiscover()->migrations($migrator)); + $this->app->resolving(Gate::class, fn(Gate $gate) => $this->autodiscover()->policies($gate)); - // Look for and register all our commands in the CLI context - Artisan::starting(Closure::fromCallable([$this, 'onArtisanStart'])); + Artisan::starting(function(Artisan $artisan) { + $this->autodiscover()->commands($artisan); + $this->registerNamespacesInTinker(); + }); } public function boot(): void @@ -78,7 +84,6 @@ public function boot(): void $this->bootPackageCommands(); $this->bootRoutes(); - $this->bootBreadcrumbs(); $this->bootViews(); $this->bootBladeComponents(); $this->bootTranslations(); @@ -90,9 +95,9 @@ protected function registry(): ModuleRegistry return $this->registry ??= $this->app->make(ModuleRegistry::class); } - protected function autoDiscoveryHelper(): AutoDiscoveryHelper + protected function autodiscover(): AutodiscoveryHelper { - return $this->auto_discovery_helper ??= $this->app->make(AutoDiscoveryHelper::class); + return $this->autodiscovery_helper ??= $this->app->make(AutodiscoveryHelper::class); } protected function publishVendorFiles(): void @@ -104,139 +109,52 @@ protected function publishVendorFiles(): void protected function bootPackageCommands(): void { - if (! $this->app->runningInConsole()) { - return; + if ($this->app->runningInConsole()) { + $this->commands([ + MakeModule::class, + ModulesCache::class, + ModulesClear::class, + ModulesSync::class, + ModulesList::class, + ]); } - - $this->commands([ - MakeModule::class, - ModulesCache::class, - ModulesClear::class, - ModulesSync::class, - ModulesList::class, - ]); } protected function bootRoutes(): void { - if ($this->app->routesAreCached()) { - return; + if (! $this->app->routesAreCached()) { + $this->autodiscover()->routes(); } - - $this->autoDiscoveryHelper() - ->routeFileFinder() - ->each(function(SplFileInfo $file) { - require $file->getRealPath(); - }); } protected function bootViews(): void { - $this->callAfterResolving('view', function(ViewFactory $view_factory) { - $this->autoDiscoveryHelper() - ->viewDirectoryFinder() - ->each(function(SplFileInfo $directory) use ($view_factory) { - $module = $this->registry()->moduleForPathOrFail($directory->getPath()); - $view_factory->addNamespace($module->name, $directory->getRealPath()); - }); + $this->callAfterResolving('view', function(ViewFactory $factory) { + $this->autodiscover()->views($factory); }); } protected function bootBladeComponents(): void { $this->callAfterResolving(BladeCompiler::class, function(BladeCompiler $blade) { - // Boot individual Blade components (old syntax: ``) - $this->autoDiscoveryHelper() - ->bladeComponentFileFinder() - ->each(function(SplFileInfo $component) use ($blade) { - $module = $this->registry()->moduleForPathOrFail($component->getPath()); - $fully_qualified_component = $module->pathToFullyQualifiedClassName($component->getPathname()); - $blade->component($fully_qualified_component, null, $module->name); - }); - - // Boot Blade component namespaces (new syntax: ``) - $this->autoDiscoveryHelper() - ->bladeComponentDirectoryFinder() - ->each(function(SplFileInfo $component) use ($blade) { - $module = $this->registry()->moduleForPathOrFail($component->getPath()); - $blade->componentNamespace($module->qualify('View\\Components'), $module->name); - }); + $this->autodiscover()->blade($blade); }); } protected function bootTranslations(): void { $this->callAfterResolving('translator', function(TranslatorContract $translator) { - if (! $translator instanceof Translator) { - return; + if ($translator instanceof Translator) { + $this->autodiscover()->translations($translator); } - - $this->autoDiscoveryHelper() - ->langDirectoryFinder() - ->each(function(SplFileInfo $directory) use ($translator) { - $module = $this->registry()->moduleForPathOrFail($directory->getPath()); - $path = $directory->getRealPath(); - - $translator->addNamespace($module->name, $path); - $translator->addJsonPath($path); - }); }); } - /** - * This functionality is likely to go away at some point so don't rely - * on it too much. The package has been abandoned. - */ - protected function bootBreadcrumbs(): void - { - $class_name = 'Diglactic\\Breadcrumbs\\Manager'; - - if (! class_exists($class_name)) { - return; - } - - // The breadcrumbs package makes $breadcrumbs available in the scope of breadcrumb - // files, so we'll do the same for consistency-sake - $breadcrumbs = $this->app->make($class_name); - - $files = glob($this->getModulesBasePath().'/*/routes/breadcrumbs/*.php'); - - foreach ($files as $file) { - require_once $file; - } - } - protected function bootLivewireComponents(): void { - if (! class_exists(Livewire::class)) { - return; + if (class_exists(LivewireManager::class)) { + $this->autodiscover()->livewire($this->app->make(LivewireManager::class)); } - - $this->autoDiscoveryHelper() - ->livewireComponentFileFinder() - ->each(function(SplFileInfo $component) { - $module = $this->registry()->moduleForPathOrFail($component->getPath()); - - $component_name = Str::of($component->getRelativePath()) - ->explode('/') - ->filter() - ->push($component->getBasename('.php')) - ->map([Str::class, 'kebab']) - ->implode('.'); - - $fully_qualified_component = $module->pathToFullyQualifiedClassName($component->getPathname()); - - Livewire::component("{$module->name}::{$component_name}", $fully_qualified_component); - }); - } - - protected function registerMigrations(Migrator $migrator): void - { - $this->autoDiscoveryHelper() - ->migrationDirectoryFinder() - ->each(function(SplFileInfo $path) use ($migrator) { - $migrator->path($path->getRealPath()); - }); } protected function registerEloquentFactories(): void @@ -247,61 +165,13 @@ protected function registerEloquentFactories(): void EloquentFactory::guessFactoryNamesUsing($helper->factoryNameResolver()); } - protected function registerPolicies(Gate $gate): void - { - $this->autoDiscoveryHelper() - ->modelFileFinder() - ->each(function(SplFileInfo $file) use ($gate) { - $module = $this->registry()->moduleForPathOrFail($file->getPath()); - $fully_qualified_model = $module->pathToFullyQualifiedClassName($file->getPathname()); - - // First, check for a policy that maps to the full namespace of the model - // i.e. Models/Foo/Bar -> Policies/Foo/BarPolicy - $namespaced_model = Str::after($fully_qualified_model, 'Models\\'); - $namespaced_policy = rtrim($module->namespaces->first(), '\\').'\\Policies\\'.$namespaced_model.'Policy'; - if (class_exists($namespaced_policy)) { - $gate->policy($fully_qualified_model, $namespaced_policy); - } - - // If that doesn't match, try the simple mapping as well - // i.e. Models/Foo/Bar -> Policies/BarPolicy - if (false !== strpos($namespaced_model, '\\')) { - $simple_model = Str::afterLast($fully_qualified_model, '\\'); - $simple_policy = rtrim($module->namespaces->first(), '\\').'\\Policies\\'.$simple_model.'Policy'; - - if (class_exists($simple_policy)) { - $gate->policy($fully_qualified_model, $simple_policy); - } - } - }); - } - - protected function onArtisanStart(Artisan $artisan): void - { - $this->registerCommands($artisan); - $this->registerNamespacesInTinker(); - } - - protected function registerCommands(Artisan $artisan): void - { - $this->autoDiscoveryHelper() - ->commandFileFinder() - ->each(function(SplFileInfo $file) use ($artisan) { - $module = $this->registry()->moduleForPathOrFail($file->getPath()); - $class_name = $module->pathToFullyQualifiedClassName($file->getPathname()); - if ($this->isInstantiableCommand($class_name)) { - $artisan->resolve($class_name); - } - }); - } - - protected function registerNamespacesInTinker() + protected function registerNamespacesInTinker(): void { if (! class_exists('Laravel\\Tinker\\TinkerServiceProvider')) { return; } - $namespaces = app(ModuleRegistry::class) + $namespaces = $this->registry() ->modules() ->flatMap(fn(ModuleConfig $config) => $config->namespaces) ->reject(fn($ns) => Str::endsWith($ns, ['Tests\\', 'Database\\Factories\\', 'Database\\Seeders\\'])) @@ -311,13 +181,6 @@ protected function registerNamespacesInTinker() Config::set('tinker.alias', array_merge($namespaces, Config::get('tinker.alias', []))); } - protected function registerLazily(string $class_name, callable $callback): self - { - $this->app->resolving($class_name, Closure::fromCallable($callback)); - - return $this; - } - protected function getModulesBasePath(): string { if (null === $this->modules_path) { @@ -327,10 +190,4 @@ protected function getModulesBasePath(): string return $this->modules_path; } - - protected function isInstantiableCommand($command): bool - { - return is_subclass_of($command, Command::class) - && ! (new ReflectionClass($command))->isAbstract(); - } } diff --git a/src/Support/ModuleFileInfo.php b/src/Support/ModuleFileInfo.php new file mode 100644 index 0000000..351375d --- /dev/null +++ b/src/Support/ModuleFileInfo.php @@ -0,0 +1,23 @@ +module()->pathToFullyQualifiedClassName($this->getPathname()); + } + + public function module(): ModuleConfig + { + return $this->module ??= Container::getInstance() + ->make(ModuleRegistry::class) + ->moduleForPathOrFail($this->getPath()); + } +} diff --git a/tests/AutoDiscoveryHelperTest.php b/tests/AutoDiscoveryHelperTest.php index 4f4a55b..ec340fd 100644 --- a/tests/AutoDiscoveryHelperTest.php +++ b/tests/AutoDiscoveryHelperTest.php @@ -8,7 +8,7 @@ use InterNACHI\Modular\Console\Commands\Make\MakeListener; use InterNACHI\Modular\Console\Commands\Make\MakeLivewire; use InterNACHI\Modular\Console\Commands\Make\MakeModel; -use InterNACHI\Modular\Support\AutoDiscoveryHelper; +use InterNACHI\Modular\Support\FinderFactory; use InterNACHI\Modular\Support\ModuleRegistry; use InterNACHI\Modular\Tests\Concerns\WritesToAppFilesystem; use Livewire\Livewire; @@ -32,10 +32,7 @@ protected function setUp(): void $this->module1 = $this->makeModule('test-module'); $this->module2 = $this->makeModule('test-module-two'); - $this->helper = new AutoDiscoveryHelper( - new ModuleRegistry($this->getApplicationBasePath().'/app-modules', ''), - new Filesystem() - ); + $this->helper = new FinderFactory($this->getApplicationBasePath().'/app-modules'); } public function test_it_finds_commands(): void From 9f479c6fa675b1f57f795092739d60048dafec26 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 14:11:00 -0400 Subject: [PATCH 02/16] wip --- src/Support/AutodiscoveryHelper.php | 28 +++++++++++++++++++ src/Support/FinderFactory.php | 10 +++++++ src/Support/ModularServiceProvider.php | 9 ++++--- src/Support/ModuleConfig.php | 5 +--- src/Support/ModuleRegistry.php | 37 ++++---------------------- 5 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 7df9e3b..0ee08cd 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Database\Migrations\Migrator; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\Str; use Illuminate\Translation\Translator; @@ -29,6 +30,33 @@ public function __construct( ) { } + /** @return Collection */ + public function modules(): Collection + { + $data = $this->withCache( + key: 'modules', + default: fn() => $this->finders + ->moduleComposerFileFinder() + ->map(function(SplFileInfo $file) { + $composer_config = json_decode($file->getContents(), true, 16, JSON_THROW_ON_ERROR); + $base_path = rtrim(str_replace('\\', '/', $file->getPath()), '/'); + + return [ + 'name' => basename($base_path), + '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) + ->mapWithKeys(fn(array $data) => [ + $data['name'] => new ModuleConfig($data['name'], $data['base_path'], new Collection($data['namespaces'])), + ]); + } + public function routes(): void { $this->withCache( diff --git a/src/Support/FinderFactory.php b/src/Support/FinderFactory.php index 9270865..e53c9cc 100644 --- a/src/Support/FinderFactory.php +++ b/src/Support/FinderFactory.php @@ -2,6 +2,8 @@ namespace InterNACHI\Modular\Support; +use Symfony\Component\Finder\SplFileInfo; + class FinderFactory { public function __construct( @@ -9,6 +11,14 @@ public function __construct( ) { } + public function moduleComposerFileFinder(): FinderCollection + { + return FinderCollection::forFiles() + ->depth('== 1') + ->name('composer.json') + ->in($this->base_path); + } + public function commandFileFinder(): FinderCollection { return FinderCollection::forFiles() diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index b2aded0..3a82269 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Contracts\Filesystem\Filesystem; +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; @@ -44,10 +45,10 @@ public function register(): void { $this->mergeConfigFrom("{$this->base_dir}/config.php", 'app-modules'); - $this->app->singleton(ModuleRegistry::class, function() { + $this->app->singleton(ModuleRegistry::class, function(Application $app) { return new ModuleRegistry( $this->getModulesBasePath(), - $this->app->bootstrapPath('cache/modules.php') // FIXME + $app->make(AutodiscoveryHelper::class), ); }); @@ -55,7 +56,7 @@ public function register(): void return new FinderFactory($this->getModulesBasePath()); }); - $this->app->singleton(AutodiscoveryHelper::class, function($app) { + $this->app->singleton(AutodiscoveryHelper::class, function(Application $app) { return new AutodiscoveryHelper( $app->make(FinderFactory::class), $app->make(Filesystem::class), @@ -63,7 +64,7 @@ public function register(): void ); }); - $this->app->singleton(MakeMigration::class, function($app) { + $this->app->singleton(MakeMigration::class, function(Application $app) { return new MigrateMakeCommand($app['migration.creator'], $app['composer']); }); diff --git a/src/Support/ModuleConfig.php b/src/Support/ModuleConfig.php index b7eb46c..59807be 100644 --- a/src/Support/ModuleConfig.php +++ b/src/Support/ModuleConfig.php @@ -10,8 +10,6 @@ class ModuleConfig implements Arrayable { - public Collection $namespaces; - public static function fromComposerFile(SplFileInfo $composer_file): self { $composer_config = json_decode($composer_file->getContents(), true, 16, JSON_THROW_ON_ERROR); @@ -32,9 +30,8 @@ public static function fromComposerFile(SplFileInfo $composer_file): self public function __construct( public string $name, public string $base_path, - ?Collection $namespaces = null + public Collection $namespaces = new Collection(), ) { - $this->namespaces = $namespaces ?? new Collection(); } public function path(string $to = ''): string diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index 82c09c9..d0e8c75 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -13,7 +13,7 @@ class ModuleRegistry public function __construct( protected string $modules_path, - protected string $cache_path + protected AutodiscoveryHelper $autodiscovery_helper, ) { } @@ -24,15 +24,13 @@ public function getModulesPath(): string public function getCachePath(): string { - return $this->cache_path; + // FIXME } public function module(?string $name = null): ?ModuleConfig { // We want to allow for gracefully handling empty/null names - return $name - ? $this->modules()->get($name) - : null; + return $name ? $this->modules()->get($name) : null; } public function moduleForPath(string $path): ?ModuleConfig @@ -64,39 +62,14 @@ public function moduleForClass(string $fqcn): ?ModuleConfig public function modules(): Collection { - return $this->modules ??= $this->loadModules(); + return $this->modules ??= $this->autodiscovery_helper->modules(); } public function reload(): Collection { $this->modules = null; - return $this->loadModules(); - } - - protected function loadModules(): Collection - { - if (file_exists($this->cache_path)) { - return Collection::make(require $this->cache_path) - ->mapWithKeys(function(array $cached) { - $config = new ModuleConfig($cached['name'], $cached['base_path'], new Collection($cached['namespaces'])); - return [$config->name => $config]; - }); - } - - if (! is_dir($this->modules_path)) { - return new Collection(); - } - - return FinderCollection::forFiles() - ->depth('== 1') - ->name('composer.json') - ->in($this->modules_path) - ->collect() - ->mapWithKeys(function(SplFileInfo $path) { - $config = ModuleConfig::fromComposerFile($path); - return [$config->name => $config]; - }); + return $this->modules(); } protected function extractModuleNameFromPath(string $path): string From 9f76c1f711968fcb2a643f602ebd2a84546dd18f Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 15:04:28 -0400 Subject: [PATCH 03/16] wip --- src/Console/Commands/ModulesCache.php | 21 ++------ src/Console/Commands/ModulesClear.php | 8 +-- src/Support/AutodiscoveryHelper.php | 78 ++++++++++++++++++++++----- src/Support/FinderCollection.php | 15 +++--- src/Support/FinderFactory.php | 2 +- src/Support/ModuleFileInfo.php | 16 +++++- src/Support/ModuleRegistry.php | 2 +- 7 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/Console/Commands/ModulesCache.php b/src/Console/Commands/ModulesCache.php index 1efe3bc..480dd82 100644 --- a/src/Console/Commands/ModulesCache.php +++ b/src/Console/Commands/ModulesCache.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use InterNACHI\Modular\Support\AutodiscoveryHelper; use InterNACHI\Modular\Support\ModuleConfig; use InterNACHI\Modular\Support\ModuleRegistry; use LogicException; @@ -15,27 +16,11 @@ class ModulesCache extends Command protected $description = 'Create a cache file for faster module loading'; - public function handle(ModuleRegistry $registry, Filesystem $filesystem) + public function handle(AutodiscoveryHelper $helper) { $this->call(ModulesClear::class); - $export = $registry->modules() - ->map(function(ModuleConfig $module_config) { - return $module_config->toArray(); - }) - ->toArray(); - - $cache_path = $registry->getCachePath(); - $cache_contents = 'put($cache_path, $cache_contents); - - try { - require $cache_path; - } catch (Throwable $e) { - $filesystem->delete($cache_path); - throw new LogicException('Unable to cache module configuration.', 0, $e); - } + $helper->writeCache($this->getLaravel()); $this->info('Modules cached successfully!'); } diff --git a/src/Console/Commands/ModulesClear.php b/src/Console/Commands/ModulesClear.php index 08b2976..fcad824 100644 --- a/src/Console/Commands/ModulesClear.php +++ b/src/Console/Commands/ModulesClear.php @@ -3,8 +3,7 @@ namespace InterNACHI\Modular\Console\Commands; use Illuminate\Console\Command; -use Illuminate\Filesystem\Filesystem; -use InterNACHI\Modular\Support\ModuleRegistry; +use InterNACHI\Modular\Support\AutodiscoveryHelper; class ModulesClear extends Command { @@ -12,9 +11,10 @@ class ModulesClear extends Command protected $description = 'Remove the module cache file'; - public function handle(Filesystem $filesystem, ModuleRegistry $registry) + public function handle(AutodiscoveryHelper $helper) { - $filesystem->delete($registry->getCachePath()); + $helper->clearCache(); + $this->info('Module cache cleared!'); } } diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 0ee08cd..eb5d052 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -6,16 +6,17 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Console\Command; use Illuminate\Contracts\Auth\Access\Gate; +use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Database\Migrations\Migrator; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; -use Illuminate\Support\Enumerable; use Illuminate\Support\Str; use Illuminate\Translation\Translator; use Illuminate\View\Compilers\BladeCompiler; use Illuminate\View\Factory as ViewFactory; use Livewire\LivewireManager; use ReflectionClass; +use RuntimeException; use Symfony\Component\Finder\SplFileInfo; use Throwable; @@ -30,9 +31,57 @@ public function __construct( ) { } + public function writeCache(Container $app): void + { + $helpers = [ + $this->modules(...), + $this->routes(...), + $this->views(...), + $this->blade(...), + $this->translations(...), + $this->migrations(...), + $this->commands(...), + $this->policies(...), + function() use ($app) { + if (class_exists(LivewireManager::class)) { + $this->livewire($app->make(LivewireManager::class)); + } + }, + ]; + + foreach ($helpers as $helper) { + $app->call($helper); + } + + $cache = Collection::make($this->data)->toArray(); + $php = 'filesystem->put($this->cache_path, $php)) { + throw new RuntimeException('Unable to write cache file.'); + } + + try { + require $this->cache_path; + } catch (Throwable $e) { + $this->filesystem->delete($this->cache_path); + throw new RuntimeException('Attempted to write invalid cache file.', $e->getCode(), $e); + } + } + + public function clearCache(): void + { + if ($this->filesystem->exists($this->cache_path)) { + $this->filesystem->delete($this->cache_path); + } + } + /** @return Collection */ - public function modules(): Collection + public function modules(bool $reload = false): Collection { + if ($reload) { + unset($this->data['modules']); + } + $data = $this->withCache( key: 'modules', default: fn() => $this->finders @@ -72,7 +121,7 @@ public function views(ViewFactory $factory): void { $this->withCache( key: 'view_namespaces', - default: $this->finders + default: fn() => $this->finders ->viewDirectoryFinder() ->withModuleInfo() ->map(fn(ModuleFileInfo $dir) => [ @@ -88,7 +137,7 @@ public function blade(BladeCompiler $blade): void // Handle individual Blade components (old syntax: ``) $this->withCache( key: 'blade_component_files', - default: $this->finders + default: fn() => $this->finders ->bladeComponentFileFinder() ->withModuleInfo() ->map(fn(ModuleFileInfo $component) => [ @@ -101,7 +150,7 @@ public function blade(BladeCompiler $blade): void // Handle Blade component namespaces (new syntax: ``) $this->withCache( key: 'blade_component_dirs', - default: $this->finders + default: fn() => $this->finders ->bladeComponentDirectoryFinder() ->withModuleInfo() ->map(fn(ModuleFileInfo $component) => [ @@ -115,9 +164,10 @@ public function blade(BladeCompiler $blade): void public function translations(Translator $translator): void { $this->withCache( - key: 'blade_component_files', - default: $this->finders + key: 'translation_files', + default: fn() => $this->finders ->langDirectoryFinder() + ->withModuleInfo() ->map(fn(ModuleFileInfo $dir) => [ 'namespace' => $dir->module()->name, 'path' => $dir->getRealPath(), @@ -133,7 +183,7 @@ public function migrations(Migrator $migrator): void { $this->withCache( key: 'migration_files', - default: $this->finders + default: fn() => $this->finders ->migrationDirectoryFinder() ->map(fn(SplFileInfo $file) => $file->getRealPath()), each: fn(string $path) => $migrator->path($path), @@ -144,7 +194,7 @@ public function commands(Artisan $artisan): void { $this->withCache( key: 'command_files', - default: $this->finders + default: fn() => $this->finders ->commandFileFinder() ->withModuleInfo() ->map(fn(ModuleFileInfo $file) => $file->fullyQualifiedClassName()) @@ -157,7 +207,7 @@ public function policies(Gate $gate): void { $this->withCache( key: 'model_policy_files', - default: $this->finders + default: fn() => $this->finders ->modelFileFinder() ->withModuleInfo() ->map(function(ModuleFileInfo $file) use ($gate) { @@ -189,7 +239,7 @@ public function livewire(LivewireManager $livewire): void { $this->withCache( key: 'livewire_component_files', - default: $this->finders + default: fn() => $this->finders ->livewireComponentFileFinder() ->withModuleInfo() ->map(fn(ModuleFileInfo $file) => [ @@ -213,12 +263,12 @@ protected function withCache( string $key, Closure $default, ?Closure $each = null, - ): Enumerable|array { + ): iterable { $this->data ??= $this->readData(); $this->data[$key] ??= value($default); return $each - ? collect($this->data[$key])->each($each) + ? Collection::make($this->data[$key])->each($each) : $this->data[$key]; } diff --git a/src/Support/FinderCollection.php b/src/Support/FinderCollection.php index abee334..e4f3380 100644 --- a/src/Support/FinderCollection.php +++ b/src/Support/FinderCollection.php @@ -4,15 +4,17 @@ use Illuminate\Support\LazyCollection; use Illuminate\Support\Traits\ForwardsCalls; +use IteratorAggregate; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; +use Traversable; /** * @mixin \Illuminate\Support\LazyCollection * @mixin \Symfony\Component\Finder\Finder */ -class FinderCollection +class FinderCollection implements IteratorAggregate { use ForwardsCalls; @@ -48,11 +50,12 @@ public function inOrEmpty(string|array $dirs): static public function withModuleInfo(): static { - return $this->map(fn(SplFileInfo $file) => new ModuleFileInfo( - $file->getFilename(), - $file->getRelativePath(), - $file->getRelativePathname(), - )); + return $this->map(fn(SplFileInfo $file) => new ModuleFileInfo($file)); + } + + public function getIterator(): Traversable + { + return $this->forwardCollection()->getIterator(); } public function __call($name, $arguments) diff --git a/src/Support/FinderFactory.php b/src/Support/FinderFactory.php index e53c9cc..1fae7c9 100644 --- a/src/Support/FinderFactory.php +++ b/src/Support/FinderFactory.php @@ -16,7 +16,7 @@ public function moduleComposerFileFinder(): FinderCollection return FinderCollection::forFiles() ->depth('== 1') ->name('composer.json') - ->in($this->base_path); + ->inOrEmpty($this->base_path); } public function commandFileFinder(): FinderCollection diff --git a/src/Support/ModuleFileInfo.php b/src/Support/ModuleFileInfo.php index 351375d..a296d4c 100644 --- a/src/Support/ModuleFileInfo.php +++ b/src/Support/ModuleFileInfo.php @@ -3,12 +3,21 @@ namespace InterNACHI\Modular\Support; use Illuminate\Container\Container; +use Illuminate\Support\Traits\ForwardsCalls; use Symfony\Component\Finder\SplFileInfo; -class ModuleFileInfo extends SplFileInfo +/** @mixin SplFileInfo */ +class ModuleFileInfo { + use ForwardsCalls; + protected ?ModuleConfig $module = null; + public function __construct( + protected SplFileInfo $file, + ) { + } + public function fullyQualifiedClassName(): string { return $this->module()->pathToFullyQualifiedClassName($this->getPathname()); @@ -20,4 +29,9 @@ public function module(): ModuleConfig ->make(ModuleRegistry::class) ->moduleForPathOrFail($this->getPath()); } + + public function __call(string $name, array $arguments) + { + return $this->forwardDecoratedCallTo($this->file, $name, $arguments); + } } diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index d0e8c75..59ab6de 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -69,7 +69,7 @@ public function reload(): Collection { $this->modules = null; - return $this->modules(); + return $this->modules ??= $this->autodiscovery_helper->modules(reload: true); } protected function extractModuleNameFromPath(string $path): string From 2368c9bae4c4af096e26274ef793226aee1cb1e8 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 16:24:17 -0400 Subject: [PATCH 04/16] wip --- src/Console/Commands/Make/MakeModule.php | 16 +++---- src/Console/Commands/Make/Modularize.php | 20 ++++---- src/Console/Commands/ModulesCache.php | 5 -- src/Support/AutodiscoveryHelper.php | 58 +++++++++++++++--------- src/Support/Cache.php | 3 +- src/Support/FinderCollection.php | 8 +++- src/Support/FinderFactory.php | 8 ++-- src/Support/ModularServiceProvider.php | 2 +- src/Support/ModuleRegistry.php | 5 +- src/Support/PhpStorm/ConfigWriter.php | 4 +- tests/Commands/ModulesCacheTest.php | 30 +++++++----- tests/Commands/ModulesClearTest.php | 2 +- 12 files changed, 91 insertions(+), 70 deletions(-) diff --git a/src/Console/Commands/Make/MakeModule.php b/src/Console/Commands/Make/MakeModule.php index 0dcdd61..997f4d6 100644 --- a/src/Console/Commands/Make/MakeModule.php +++ b/src/Console/Commands/Make/MakeModule.php @@ -94,13 +94,13 @@ public function handle() $this->setUpStyles(); $this->newLine(); - + if ($this->shouldAbortToPublishConfig()) { return 0; } - + $this->ensureModulesDirectoryExists(); - + $this->writeStubs(); $this->updateCoreComposerConfig(); @@ -115,6 +115,11 @@ public function handle() return 0; } + public function newLine($count = 1) + { + $this->getOutput()->newLine($count); + } + protected function shouldAbortToPublishConfig(): bool { if ( @@ -315,11 +320,6 @@ protected function title($title) $this->getOutput()->title($title); } - public function newLine($count = 1) - { - $this->getOutput()->newLine($count); - } - protected function getStubs(): array { if (is_array($custom_stubs = config('app-modules.stubs'))) { diff --git a/src/Console/Commands/Make/Modularize.php b/src/Console/Commands/Make/Modularize.php index a4aad98..91866a3 100644 --- a/src/Console/Commands/Make/Modularize.php +++ b/src/Console/Commands/Make/Modularize.php @@ -8,6 +8,16 @@ trait Modularize { use \InterNACHI\Modular\Console\Commands\Modularize; + public function call($command, array $arguments = []) + { + // Pass the --module flag on to subsequent commands + if ($module = $this->option('module')) { + $arguments['--module'] = $module; + } + + return $this->runCommand($command, $arguments, $this->output); + } + protected function getDefaultNamespace($rootNamespace) { $namespace = parent::getDefaultNamespace($rootNamespace); @@ -80,14 +90,4 @@ protected function getPath($name) return $path; } - - public function call($command, array $arguments = []) - { - // Pass the --module flag on to subsequent commands - if ($module = $this->option('module')) { - $arguments['--module'] = $module; - } - - return $this->runCommand($command, $arguments, $this->output); - } } diff --git a/src/Console/Commands/ModulesCache.php b/src/Console/Commands/ModulesCache.php index 480dd82..47a93ab 100644 --- a/src/Console/Commands/ModulesCache.php +++ b/src/Console/Commands/ModulesCache.php @@ -3,12 +3,7 @@ namespace InterNACHI\Modular\Console\Commands; use Illuminate\Console\Command; -use Illuminate\Filesystem\Filesystem; use InterNACHI\Modular\Support\AutodiscoveryHelper; -use InterNACHI\Modular\Support\ModuleConfig; -use InterNACHI\Modular\Support\ModuleRegistry; -use LogicException; -use Throwable; class ModulesCache extends Command { diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index eb5d052..9483362 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -6,9 +6,10 @@ 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\Filesystem\Filesystem; use Illuminate\Database\Migrations\Migrator; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Translation\Translator; @@ -26,7 +27,7 @@ class AutodiscoveryHelper public function __construct( protected FinderFactory $finders, - protected Filesystem $filesystem, + protected Filesystem $fs, protected string $cache_path, ) { } @@ -42,36 +43,38 @@ public function writeCache(Container $app): void $this->migrations(...), $this->commands(...), $this->policies(...), - function() use ($app) { - if (class_exists(LivewireManager::class)) { - $this->livewire($app->make(LivewireManager::class)); - } - }, + $this->livewire(...), ]; foreach ($helpers as $helper) { - $app->call($helper); + try { + $app->call($helper); + } catch (BindingResolutionException) { + // + } } $cache = Collection::make($this->data)->toArray(); $php = 'filesystem->put($this->cache_path, $php)) { + $this->fs->ensureDirectoryExists($this->fs->dirname($this->cache_path)); + + if (! $this->fs->put($this->cache_path, $php)) { throw new RuntimeException('Unable to write cache file.'); } try { require $this->cache_path; } catch (Throwable $e) { - $this->filesystem->delete($this->cache_path); + $this->fs->delete($this->cache_path); throw new RuntimeException('Attempted to write invalid cache file.', $e->getCode(), $e); } } public function clearCache(): void { - if ($this->filesystem->exists($this->cache_path)) { - $this->filesystem->delete($this->cache_path); + if ($this->fs->exists($this->cache_path)) { + $this->fs->delete($this->cache_path); } } @@ -86,24 +89,26 @@ public function modules(bool $reload = false): Collection key: 'modules', default: fn() => $this->finders ->moduleComposerFileFinder() - ->map(function(SplFileInfo $file) { + ->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' => basename($base_path), - 'base_path' => $base_path, - 'namespaces' => Collection::make($composer_config['autoload']['psr-4'] ?? []) - ->mapWithKeys(fn($src, $namespace) => ["{$base_path}/{$src}" => $namespace]) - ->all(), + $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) - ->mapWithKeys(fn(array $data) => [ - $data['name'] => new ModuleConfig($data['name'], $data['base_path'], new Collection($data['namespaces'])), - ]); + ->map(fn(array $d) => new ModuleConfig($d['name'], $d['base_path'], new Collection($d['namespaces']))); } public function routes(): void @@ -112,6 +117,7 @@ public function routes(): void key: 'route_files', default: fn() => $this->finders ->routeFileFinder() + ->values() ->map(fn(SplFileInfo $file) => $file->getRealPath()), each: fn(string $filename) => require $filename ); @@ -124,6 +130,7 @@ public function views(ViewFactory $factory): void default: fn() => $this->finders ->viewDirectoryFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $dir) => [ 'namespace' => $dir->module()->name, 'path' => $dir->getRealPath(), @@ -140,6 +147,7 @@ public function blade(BladeCompiler $blade): void default: fn() => $this->finders ->bladeComponentFileFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $component) => [ 'prefix' => $component->module()->name, 'fqcn' => $component->fullyQualifiedClassName(), @@ -153,6 +161,7 @@ public function blade(BladeCompiler $blade): void default: fn() => $this->finders ->bladeComponentDirectoryFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $component) => [ 'prefix' => $component->module()->name, 'namespace' => $component->module()->qualify('View\\Components'), @@ -168,6 +177,7 @@ public function translations(Translator $translator): void default: fn() => $this->finders ->langDirectoryFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $dir) => [ 'namespace' => $dir->module()->name, 'path' => $dir->getRealPath(), @@ -185,6 +195,7 @@ public function migrations(Migrator $migrator): void key: 'migration_files', default: fn() => $this->finders ->migrationDirectoryFinder() + ->values() ->map(fn(SplFileInfo $file) => $file->getRealPath()), each: fn(string $path) => $migrator->path($path), ); @@ -197,6 +208,7 @@ public function commands(Artisan $artisan): void default: fn() => $this->finders ->commandFileFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $file) => $file->fullyQualifiedClassName()) ->filter($this->isInstantiableCommand(...)), each: fn(string $fqcn) => $artisan->resolve($fqcn), @@ -210,6 +222,7 @@ public function policies(Gate $gate): void default: fn() => $this->finders ->modelFileFinder() ->withModuleInfo() + ->values() ->map(function(ModuleFileInfo $file) use ($gate) { $fqcn = $file->fullyQualifiedClassName(); $namespace = rtrim($file->module()->namespaces->first(), '\\'); @@ -242,6 +255,7 @@ public function livewire(LivewireManager $livewire): void default: fn() => $this->finders ->livewireComponentFileFinder() ->withModuleInfo() + ->values() ->map(fn(ModuleFileInfo $file) => [ 'name' => sprintf( '%s::%s', @@ -275,7 +289,7 @@ protected function withCache( protected function readData(): array { try { - return $this->filesystem->exists($this->cache_path) + return $this->fs->exists($this->cache_path) ? require $this->cache_path : []; } catch (Throwable) { diff --git a/src/Support/Cache.php b/src/Support/Cache.php index 3d7301d..45ca7b7 100644 --- a/src/Support/Cache.php +++ b/src/Support/Cache.php @@ -2,6 +2,7 @@ namespace InterNACHI\Modular\Support; +use Closure; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -17,7 +18,7 @@ public function __construct( ) { } - public function get(string $key, \Closure $callback): Enumerable + public function get(string $key, Closure $callback): Enumerable { return Collection::make($this->data($key, $callback)); } diff --git a/src/Support/FinderCollection.php b/src/Support/FinderCollection.php index e4f3380..6e9a711 100644 --- a/src/Support/FinderCollection.php +++ b/src/Support/FinderCollection.php @@ -2,6 +2,7 @@ namespace InterNACHI\Modular\Support; +use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\LazyCollection; use Illuminate\Support\Traits\ForwardsCalls; use IteratorAggregate; @@ -14,7 +15,7 @@ * @mixin \Illuminate\Support\LazyCollection * @mixin \Symfony\Component\Finder\Finder */ -class FinderCollection implements IteratorAggregate +class FinderCollection implements Arrayable, IteratorAggregate { use ForwardsCalls; @@ -58,6 +59,11 @@ public function getIterator(): Traversable return $this->forwardCollection()->getIterator(); } + public function toArray(): array + { + return $this->forwardCollection()->toArray(); + } + public function __call($name, $arguments) { $result = $this->forwardCallTo($this->forwardCallTargetForMethod($name), $name, $arguments); diff --git a/src/Support/FinderFactory.php b/src/Support/FinderFactory.php index 1fae7c9..eba39ef 100644 --- a/src/Support/FinderFactory.php +++ b/src/Support/FinderFactory.php @@ -2,8 +2,6 @@ namespace InterNACHI\Modular\Support; -use Symfony\Component\Finder\SplFileInfo; - class FinderFactory { public function __construct( @@ -98,13 +96,13 @@ public function listenerDirectoryFinder(): FinderCollection public function livewireComponentFileFinder(): FinderCollection { $directory = $this->base_path.'/*/src'; - + if (str_contains(config('livewire.class_namespace'), '\\Http\\')) { $directory .= '/Http'; } - + $directory .= '/Livewire'; - + return FinderCollection::forFiles() ->name('*.php') ->inOrEmpty($directory); diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 3a82269..ae92ef0 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -4,12 +4,12 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Contracts\Auth\Access\Gate; -use Illuminate\Contracts\Filesystem\Filesystem; 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\Support\Facades\Config; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index 59ab6de..688267c 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -5,7 +5,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use InterNACHI\Modular\Exceptions\CannotFindModuleForPathException; -use Symfony\Component\Finder\SplFileInfo; class ModuleRegistry { @@ -30,7 +29,9 @@ public function getCachePath(): string public function module(?string $name = null): ?ModuleConfig { // We want to allow for gracefully handling empty/null names - return $name ? $this->modules()->get($name) : null; + return $name + ? $this->modules()->get($name) + : null; } public function moduleForPath(string $path): ?ModuleConfig diff --git a/src/Support/PhpStorm/ConfigWriter.php b/src/Support/PhpStorm/ConfigWriter.php index a0b9732..cb80180 100644 --- a/src/Support/PhpStorm/ConfigWriter.php +++ b/src/Support/PhpStorm/ConfigWriter.php @@ -23,14 +23,14 @@ abstract class ConfigWriter */ protected $module_registry; - abstract public function write(): bool; - public function __construct($config_path, ModuleRegistry $module_registry) { $this->config_path = $config_path; $this->module_registry = $module_registry; } + abstract public function write(): bool; + public function handle(): bool { if (! $this->checkConfigFilePermissions()) { diff --git a/tests/Commands/ModulesCacheTest.php b/tests/Commands/ModulesCacheTest.php index 87cd1a0..c817683 100644 --- a/tests/Commands/ModulesCacheTest.php +++ b/tests/Commands/ModulesCacheTest.php @@ -12,18 +12,24 @@ class ModulesCacheTest extends TestCase public function test_it_writes_to_cache_file(): void { - $this->makeModule('test-module'); - $this->makeModule('test-module-two'); + $expected_path = $this->getApplicationBasePath().$this->normalizeDirectorySeparators('bootstrap/cache/app-modules.php'); - $this->artisan(ModulesCache::class); - - $expected_path = $this->getApplicationBasePath().$this->normalizeDirectorySeparators('bootstrap/cache/modules.php'); - - $this->assertFileExists($expected_path); - - $cache = include $expected_path; - - $this->assertArrayHasKey('test-module', $cache); - $this->assertArrayHasKey('test-module-two', $cache); + try { + $this->makeModule('test-module'); + $this->makeModule('test-module-two'); + + $this->artisan(ModulesCache::class); + + $this->assertFileExists($expected_path); + + $cache = include $expected_path; + + $this->assertArrayHasKey('test-module', $cache['modules']); + $this->assertArrayHasKey('test-module-two', $cache['modules']); + } finally { + if (file_exists($expected_path)) { + unlink($expected_path); + } + } } } diff --git a/tests/Commands/ModulesClearTest.php b/tests/Commands/ModulesClearTest.php index d89d6a5..21c1a87 100644 --- a/tests/Commands/ModulesClearTest.php +++ b/tests/Commands/ModulesClearTest.php @@ -15,7 +15,7 @@ public function test_it_writes_to_cache_file(): void { $this->artisan(ModulesCache::class); - $expected_path = $this->getApplicationBasePath().$this->normalizeDirectorySeparators('bootstrap/cache/modules.php'); + $expected_path = $this->getApplicationBasePath().$this->normalizeDirectorySeparators('bootstrap/cache/app-modules.php'); $this->assertFileExists($expected_path); From 9675cc4f6e630b41ecdc752afd44d939a91e2954 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 16:25:47 -0400 Subject: [PATCH 05/16] Move config --- config.php => config/app-modules.php | 0 src/Support/ModularServiceProvider.php | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename config.php => config/app-modules.php (100%) diff --git a/config.php b/config/app-modules.php similarity index 100% rename from config.php rename to config/app-modules.php diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index ae92ef0..223c7d7 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -43,7 +43,7 @@ public function __construct($app) public function register(): void { - $this->mergeConfigFrom("{$this->base_dir}/config.php", 'app-modules'); + $this->mergeConfigFrom("{$this->base_dir}/config/app-modules.php", 'app-modules'); $this->app->singleton(ModuleRegistry::class, function(Application $app) { return new ModuleRegistry( @@ -104,7 +104,7 @@ protected function autodiscover(): AutodiscoveryHelper protected function publishVendorFiles(): void { $this->publishes([ - "{$this->base_dir}/config.php" => $this->app->configPath('app-modules.php'), + "{$this->base_dir}/config/app-modules.php" => $this->app->configPath('app-modules.php'), ], 'modular-config'); } From 483bd28e4fbf765fe4f37a46c8c3282c7c94217e Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 16:31:54 -0400 Subject: [PATCH 06/16] Delete Cache.php --- src/Support/Cache.php | 41 ----------------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 src/Support/Cache.php diff --git a/src/Support/Cache.php b/src/Support/Cache.php deleted file mode 100644 index 45ca7b7..0000000 --- a/src/Support/Cache.php +++ /dev/null @@ -1,41 +0,0 @@ -data($key, $callback)); - } - - public function save(): bool - { - $cache_contents = 'data, true).';'.PHP_EOL; - - return $this->filesystem->put($this->path, $cache_contents); - } - - protected function data(?string $key = null, mixed $default = null): array - { - if (null === $this->data && $this->filesystem->exists($this->path)) { - $this->data = require $this->path; - } - - return Arr::get($this->data, $key, $default); - } -} From 62b4ee0fdfa46d66bab733584f66a1edea8bcda1 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 16:33:25 -0400 Subject: [PATCH 07/16] Code style --- src/Support/AutodiscoveryHelper.php | 4 ++-- tests/AutoDiscoveryHelperTest.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index 9483362..f2144d3 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -50,12 +50,12 @@ public function writeCache(Container $app): void try { $app->call($helper); } catch (BindingResolutionException) { - // + } } $cache = Collection::make($this->data)->toArray(); - $php = 'fs->ensureDirectoryExists($this->fs->dirname($this->cache_path)); diff --git a/tests/AutoDiscoveryHelperTest.php b/tests/AutoDiscoveryHelperTest.php index ec340fd..8d3865f 100644 --- a/tests/AutoDiscoveryHelperTest.php +++ b/tests/AutoDiscoveryHelperTest.php @@ -9,7 +9,6 @@ use InterNACHI\Modular\Console\Commands\Make\MakeLivewire; use InterNACHI\Modular\Console\Commands\Make\MakeModel; use InterNACHI\Modular\Support\FinderFactory; -use InterNACHI\Modular\Support\ModuleRegistry; use InterNACHI\Modular\Tests\Concerns\WritesToAppFilesystem; use Livewire\Livewire; use Livewire\LivewireServiceProvider; From a9a3d42186a0bb3be9041b774c251f66b9c2c681 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 17:07:25 -0400 Subject: [PATCH 08/16] Improve event auto-discovery --- composer.json | 3 +- src/Support/AutodiscoveryHelper.php | 24 ++++++ src/Support/ModularEventServiceProvider.php | 74 ------------------- src/Support/ModularServiceProvider.php | 24 ++++++ .../EventDiscoveryExplicitlyEnabledTest.php | 16 ++-- .../EventDiscoveryImplicitlyEnabledTest.php | 14 ++-- ...l11EventDiscoveryImplicitlyEnabledTest.php | 13 ++-- tests/TestCase.php | 1 - 8 files changed, 72 insertions(+), 97 deletions(-) delete mode 100644 src/Support/ModularEventServiceProvider.php diff --git a/composer.json b/composer.json index f5bc368..f5789b3 100644 --- a/composer.json +++ b/composer.json @@ -48,8 +48,7 @@ "laravel": { "providers": [ "InterNACHI\\Modular\\Support\\ModularServiceProvider", - "InterNACHI\\Modular\\Support\\ModularizedCommandsServiceProvider", - "InterNACHI\\Modular\\Support\\ModularEventServiceProvider" + "InterNACHI\\Modular\\Support\\ModularizedCommandsServiceProvider" ], "aliases": { "Modules": "InterNACHI\\Modular\\Support\\Facades\\Modules" diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index f2144d3..eaa7ed7 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -8,6 +8,7 @@ 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; @@ -248,6 +249,29 @@ public function policies(Gate $gate): void ); } + 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( diff --git a/src/Support/ModularEventServiceProvider.php b/src/Support/ModularEventServiceProvider.php deleted file mode 100644 index e7300f8..0000000 --- a/src/Support/ModularEventServiceProvider.php +++ /dev/null @@ -1,74 +0,0 @@ -app->booting(function() { - $events = $this->getEvents(); - $provider = Arr::first($this->app->getProviders(EventServiceProvider::class)); - - if (! $provider || empty($events)) { - return; - } - - $listen = new ReflectionProperty($provider, 'listen'); - $listen->setAccessible(true); - $listen->setValue($provider, array_merge_recursive($listen->getValue($provider), $events)); - }); - } - - public function getEvents(): array - { - // If events are cached, or Modular event discovery is disabled, then we'll - // just let the normal event service provider handle all the event loading. - if ($this->app->eventsAreCached() || ! $this->shouldDiscoverEvents()) { - return []; - } - - return $this->discoverEvents(); - } - - public function shouldDiscoverEvents(): bool - { - return config('app-modules.should_discover_events') - ?? $this->appIsConfiguredToDiscoverEvents(); - } - - public function discoverEvents() - { - $modules = $this->app->make(ModuleRegistry::class); - - return $this->app->make(FinderFactory::class) - ->listenerDirectoryFinder() - ->map(fn(SplFileInfo $directory) => $directory->getPathname()) - ->reduce(function($discovered, string $directory) use ($modules) { - $module = $modules->moduleForPath($directory); - return array_merge_recursive( - $discovered, - DiscoverEvents::within($directory, $module->path('src')) - ); - }, []); - } - - public 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/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 223c7d7..34341ab 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -4,12 +4,14 @@ 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; @@ -88,6 +90,7 @@ public function boot(): void $this->bootViews(); $this->bootBladeComponents(); $this->bootTranslations(); + $this->bootEvents(); $this->bootLivewireComponents(); } @@ -151,6 +154,13 @@ protected function bootTranslations(): void }); } + protected function bootEvents(): void + { + $this->callAfterResolving(Dispatcher::class, function(Dispatcher $events) { + $this->autodiscover()->events($events, $this->shouldDiscoverEvents()); + }); + } + protected function bootLivewireComponents(): void { if (class_exists(LivewireManager::class)) { @@ -191,4 +201,18 @@ 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/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php b/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php index 68793d7..6268384 100644 --- a/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php +++ b/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php @@ -4,8 +4,11 @@ // this needs to be its own isolated test file. namespace InterNACHI\Modular\Tests\EventDiscovery { + use App\EventDiscoveryExplicitlyEnabledTestProvider; use Illuminate\Support\Facades\Event; + use InterNACHI\Modular\Console\Commands\ModulesCache; + use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Support\Facades\Modules; use InterNACHI\Modular\Tests\Concerns\PreloadsAppModules; use InterNACHI\Modular\Tests\TestCase; @@ -18,7 +21,7 @@ protected function setUp(): void { parent::setUp(); - $this->beforeApplicationDestroyed(fn() => $this->artisan('event:clear')); + $this->beforeApplicationDestroyed(fn() => $this->artisan(ModulesClear::class)); } public function test_it_auto_discovers_event_listeners(): void @@ -29,18 +32,16 @@ public function test_it_auto_discovers_event_listeners(): void // Also check that the events are cached correctly - $this->artisan('event:cache'); + $this->artisan(ModulesCache::class); - $cache = require $this->app->getCachedEventsPath(); + $cache = require $this->app->bootstrapPath('cache/app-modules.php'); - $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cache[EventDiscoveryExplicitlyEnabledTestProvider::class]); + $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cache['events']); $this->assertContains( $module->qualify('Listeners\\TestEventListener@handle'), - $cache[EventDiscoveryExplicitlyEnabledTestProvider::class][$module->qualify('Events\\TestEvent')] + $cache['events'][$module->qualify('Events\\TestEvent')] ); - - $this->artisan('event:clear'); } protected function getPackageProviders($app) @@ -60,6 +61,7 @@ protected function resolveApplicationConfiguration($app) // We need to use an "App" namespace to tell modular that this provider should be deferred to namespace App { + use Illuminate\Foundation\Support\Providers\EventServiceProvider; class EventDiscoveryExplicitlyEnabledTestProvider extends EventServiceProvider diff --git a/tests/EventDiscovery/EventDiscoveryImplicitlyEnabledTest.php b/tests/EventDiscovery/EventDiscoveryImplicitlyEnabledTest.php index 966480c..6ad5514 100644 --- a/tests/EventDiscovery/EventDiscoveryImplicitlyEnabledTest.php +++ b/tests/EventDiscovery/EventDiscoveryImplicitlyEnabledTest.php @@ -6,6 +6,8 @@ namespace InterNACHI\Modular\Tests\EventDiscovery { use App\EventDiscoveryImplicitlyEnabledTestProvider; use Illuminate\Support\Facades\Event; + use InterNACHI\Modular\Console\Commands\ModulesCache; + use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Support\Facades\Modules; use InterNACHI\Modular\Tests\Concerns\PreloadsAppModules; use InterNACHI\Modular\Tests\TestCase; @@ -18,7 +20,7 @@ protected function setUp(): void { parent::setUp(); - $this->beforeApplicationDestroyed(fn() => $this->artisan('event:clear')); + $this->beforeApplicationDestroyed(fn() => $this->artisan(ModulesClear::class)); } public function test_it_auto_discovers_event_listeners(): void @@ -29,18 +31,16 @@ public function test_it_auto_discovers_event_listeners(): void // Also check that the events are cached correctly - $this->artisan('event:cache'); + $this->artisan(ModulesCache::class); - $cache = require $this->app->getCachedEventsPath(); + $cache = require $this->app->bootstrapPath('cache/app-modules.php'); - $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cache[EventDiscoveryImplicitlyEnabledTestProvider::class]); + $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cache['events']); $this->assertContains( $module->qualify('Listeners\\TestEventListener@handle'), - $cache[EventDiscoveryImplicitlyEnabledTestProvider::class][$module->qualify('Events\\TestEvent')] + $cache['events'][$module->qualify('Events\\TestEvent')] ); - - $this->artisan('event:clear'); } protected function getPackageProviders($app) diff --git a/tests/EventDiscovery/Laravel11EventDiscoveryImplicitlyEnabledTest.php b/tests/EventDiscovery/Laravel11EventDiscoveryImplicitlyEnabledTest.php index ea87b19..f5d83c1 100644 --- a/tests/EventDiscovery/Laravel11EventDiscoveryImplicitlyEnabledTest.php +++ b/tests/EventDiscovery/Laravel11EventDiscoveryImplicitlyEnabledTest.php @@ -4,6 +4,8 @@ use Illuminate\Foundation\Support\Providers\EventServiceProvider; use Illuminate\Support\Facades\Event; +use InterNACHI\Modular\Console\Commands\ModulesCache; +use InterNACHI\Modular\Console\Commands\ModulesClear; use InterNACHI\Modular\Support\Facades\Modules; use InterNACHI\Modular\Tests\Concerns\PreloadsAppModules; use InterNACHI\Modular\Tests\TestCase; @@ -16,7 +18,7 @@ protected function setUp(): void { parent::setUp(); - $this->beforeApplicationDestroyed(fn() => $this->artisan('event:clear')); + $this->beforeApplicationDestroyed(fn() => $this->artisan(ModulesClear::class)); $this->requiresLaravelVersion('11.0.0'); } @@ -28,16 +30,15 @@ public function test_it_auto_discovers_event_listeners(): void // Also check that the events are cached correctly - $this->artisan('event:cache'); + $this->artisan(ModulesCache::class); - $cache = require $this->app->getCachedEventsPath(); - $cached_listeners = collect($cache)->reduce(fn(array $listeners, $row) => array_merge_recursive($listeners, $row), []); + $cache = require $this->app->bootstrapPath('cache/app-modules.php'); - $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cached_listeners); + $this->assertArrayHasKey($module->qualify('Events\\TestEvent'), $cache['events']); $this->assertContains( $module->qualify('Listeners\\TestEventListener').'@handle', - $cached_listeners[$module->qualify('Events\\TestEvent')] + $cache['events'][$module->qualify('Events\\TestEvent')] ); } diff --git a/tests/TestCase.php b/tests/TestCase.php index be96324..2614c63 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -60,7 +60,6 @@ protected function getPackageProviders($app) return [ ModularServiceProvider::class, ModularizedCommandsServiceProvider::class, - ModularEventServiceProvider::class, ]; } From 295cc637dc6ad2adc92af83b11db20618027d982 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 17:08:06 -0400 Subject: [PATCH 09/16] Update phpunit.yml --- .github/workflows/phpunit.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index de5c9d2..2c07d5b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,13 +12,8 @@ jobs: matrix: dependency-version: [ stable, lowest ] os: [ ubuntu-latest, windows-latest ] - laravel: [ 10.*, 11.*, 12.* ] - php: [ 8.1, 8.2, 8.3, 8.4 ] - exclude: - - php: 8.1 - laravel: 11.* - - php: 8.1 - laravel: 12.* + laravel: [ 11.*, 12.* ] + php: [ 8.3, 8.4 ] runs-on: ${{ matrix.os }} timeout-minutes: 10 From 3af48c721fb1691292bf2ceaaf9084f198de6300 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 17:09:15 -0400 Subject: [PATCH 10/16] Remove unnecessary 'getCachePath' --- src/Support/ModuleRegistry.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Support/ModuleRegistry.php b/src/Support/ModuleRegistry.php index 688267c..528177d 100644 --- a/src/Support/ModuleRegistry.php +++ b/src/Support/ModuleRegistry.php @@ -21,11 +21,6 @@ public function getModulesPath(): string return $this->modules_path; } - public function getCachePath(): string - { - // FIXME - } - public function module(?string $name = null): ?ModuleConfig { // We want to allow for gracefully handling empty/null names From 261cfb53f08bee77b11b6e4791fbebf2fb4887ef Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 17:09:54 -0400 Subject: [PATCH 11/16] Code style --- src/Support/AutodiscoveryHelper.php | 1 - tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php | 2 -- tests/TestCase.php | 1 - 3 files changed, 4 deletions(-) diff --git a/src/Support/AutodiscoveryHelper.php b/src/Support/AutodiscoveryHelper.php index eaa7ed7..ccc9884 100644 --- a/src/Support/AutodiscoveryHelper.php +++ b/src/Support/AutodiscoveryHelper.php @@ -51,7 +51,6 @@ public function writeCache(Container $app): void try { $app->call($helper); } catch (BindingResolutionException) { - } } diff --git a/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php b/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php index 6268384..8a3ab00 100644 --- a/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php +++ b/tests/EventDiscovery/EventDiscoveryExplicitlyEnabledTest.php @@ -4,7 +4,6 @@ // this needs to be its own isolated test file. namespace InterNACHI\Modular\Tests\EventDiscovery { - use App\EventDiscoveryExplicitlyEnabledTestProvider; use Illuminate\Support\Facades\Event; use InterNACHI\Modular\Console\Commands\ModulesCache; @@ -61,7 +60,6 @@ protected function resolveApplicationConfiguration($app) // We need to use an "App" namespace to tell modular that this provider should be deferred to namespace App { - use Illuminate\Foundation\Support\Providers\EventServiceProvider; class EventDiscoveryExplicitlyEnabledTestProvider extends EventServiceProvider diff --git a/tests/TestCase.php b/tests/TestCase.php index 2614c63..bfec4dc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,7 +6,6 @@ use InterNACHI\Modular\Console\Commands\Make\MakeModule; use InterNACHI\Modular\Support\DatabaseFactoryHelper; use InterNACHI\Modular\Support\Facades\Modules; -use InterNACHI\Modular\Support\ModularEventServiceProvider; use InterNACHI\Modular\Support\ModularizedCommandsServiceProvider; use InterNACHI\Modular\Support\ModularServiceProvider; use InterNACHI\Modular\Support\ModuleConfig; From a0ee9e0e886df2a3bbc2a6fd2f2184ae0b8f1ca3 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 17:12:05 -0400 Subject: [PATCH 12/16] Update phpunit.yml --- .github/workflows/phpunit.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 2c07d5b..572b819 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -2,6 +2,9 @@ name: PHPUnit on: push: + branches: + - main + - 3.x pull_request: schedule: - cron: '0 14 * * 3' # Run Wednesdays at 2pm EST From 4ce9272c989eec7eef899c5f6480ef052af0ed56 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 19:39:58 -0400 Subject: [PATCH 13/16] Update actions --- .github/workflows/coverage.yml | 18 ++++++++++-------- .github/workflows/php-cs-fixer.yml | 23 ++++++++++++++++------- .github/workflows/phpunit.yml | 12 +++++------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f880bb2..b44e5d5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -22,13 +22,16 @@ jobs: extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv coverage: pcov + - name: Get composer cache directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: | - vendor - ${{ steps.composer-cache-files-dir.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }} + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- @@ -38,11 +41,10 @@ jobs: run: composer install --no-progress --no-interaction --prefer-dist - name: Run and publish code coverage - uses: paambaati/codeclimate-action@v5.0.0 + uses: paambaati/codeclimate-action@v9.0 env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: coverageCommand: vendor/bin/phpunit --coverage-clover ${{ github.workspace }}/clover.xml - debug: true coverageLocations: "${{github.workspace}}/clover.xml:clover" diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index d5424d4..4efb99f 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -1,6 +1,11 @@ name: Code Style -on: [ pull_request, push ] +on: + push: + branches: + - main + - 3.x + pull_request: jobs: coverage: @@ -10,21 +15,25 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.3 extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, intl, iconv + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: | - vendor - ${{ steps.composer-cache-files-dir.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }} + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 572b819..e90bbf1 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -35,23 +35,21 @@ jobs: tools: composer:v2 - name: Register composer cache directory - id: composer-cache-files-dir + id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: - path: | - vendor - ${{ steps.composer-cache-files-dir.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }} + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-composer- - name: Install dependencies run: | - composer require --no-progress --no-interaction --prefer-dist --update-with-all-dependencies --prefer-${{ matrix.dependency-version }} "illuminate/support:${{ matrix.laravel }}" + composer require --no-interaction --prefer-dist --update-with-all-dependencies --prefer-${{ matrix.dependency-version }} "illuminate/support:${{ matrix.laravel }}" - name: Execute tests run: vendor/bin/phpunit From 1a512a3042d2e6bc4ee1276bf3ff219e9b62bc2b Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 19:46:52 -0400 Subject: [PATCH 14/16] debug --- .github/workflows/phpunit.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index e90bbf1..18b6944 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -37,10 +37,14 @@ jobs: - name: Register composer cache directory id: composer-cache run: | - echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Debug composer cache directory + run: composer config cache-files-dir - name: Cache dependencies uses: actions/cache@v4 + if: ${{ steps.composer-cache.outputs.dir != '' }} with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} From 3462867190257a2a8c942709b57a748891959a63 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 19:49:02 -0400 Subject: [PATCH 15/16] More debugging --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 18b6944..8caff89 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -40,7 +40,7 @@ jobs: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Debug composer cache directory - run: composer config cache-files-dir + run: echo ${{ steps.composer-cache.outputs.dir }} - name: Cache dependencies uses: actions/cache@v4 From 8f57ac9455cc353365de1c7c94690cb8d6c700bf Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 19 Mar 2025 19:51:19 -0400 Subject: [PATCH 16/16] Update phpunit.yml --- .github/workflows/phpunit.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 8caff89..b821934 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -38,9 +38,6 @@ jobs: id: composer-cache run: | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Debug composer cache directory - run: echo ${{ steps.composer-cache.outputs.dir }} - name: Cache dependencies uses: actions/cache@v4