diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7aa0add --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.php] +indent_style = tab + +[*.json] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/CHANGELOG.md b/CHANGELOG.md index df358fc..99aa733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for auto-aliasing module classes in tinker sessions +- Added `#[OnRegister]` attribute for plugins that need to run during the registration phase +- Added `ConfigPlugin` to auto-load module config files (`config/{module-name}.php`) +- Added config file scaffold to `make:module` command ## [2.2.0] - 2024-04-05 diff --git a/CHANGES-V3.md b/CHANGES-V3.md index 3d1c331..6aba50e 100644 --- a/CHANGES-V3.md +++ b/CHANGES-V3.md @@ -56,15 +56,21 @@ abstract class Plugin | Attribute | Behavior | |-------------------------------------|---------------------------------------------| +| `#[OnRegister]` | Execute during `register()` phase | | `#[AfterResolving(Service::class)]` | Defer until service resolved | | `#[OnBoot]` | Execute during `booting()` hook | | *(none)* | Explicit call via `PluginHandler::handle()` | +Plugins that need to run early (before services are resolved) should use `#[OnRegister]`. +This is useful for configuration loading, where values must be available before other +services boot. + ### Built-in Plugins | Plugin | Trigger | Responsibility | |--------------------|---------------------------------|-------------------------------------------------| | `ModulesPlugin` | Eager | Discover `composer.json`, create `ModuleConfig` | +| `ConfigPlugin` | `OnRegister` | Load module config file (see below) | | `RoutesPlugin` | `!routesAreCached()` | Load route files | | `ViewPlugin` | `AfterResolving(ViewFactory)` | Register view namespaces | | `BladePlugin` | `AfterResolving(BladeCompiler)` | Register Blade components | @@ -74,14 +80,34 @@ abstract class Plugin | `GatePlugin` | `AfterResolving(Gate)` | Register model policies | | `ArtisanPlugin` | `Artisan::starting()` | Register commands | -## Boot Flow +### Module Config Files + +Each module can have a config file at `config/{module-name}.php`. The file **must** be named +after the module (e.g., `app-modules/orders/config/orders.php`). Config values are then +accessible via `config('orders.key')`. + +``` +app-modules/ +└── orders/ + └── config/ + └── orders.php ← config('orders.*') +``` + +This naming convention provides implicit namespacing since Laravel's config system is flat. +App-level config (in `config/orders.php`) takes precedence over module defaults, following +the same pattern as `mergeConfigFrom()` in Laravel packages. + +## Lifecycle Flow ``` ModularServiceProvider::register() ├─ Register singletons ├─ PluginRegistry::add(built-in plugins) + ├─ PluginHandler::register(app) + │ └─ For each plugin with #[OnRegister]: + │ └─ Plugin::handle(data) └─ $app->booting(PluginHandler::boot) - └─ For each plugin: + └─ For each plugin with boot attributes: └─ Plugin::boot(handler, app) └─ Read attributes, schedule execution ``` diff --git a/README.md b/README.md index 74d6945..d1a76a5 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,8 @@ Modular will scaffold up a new module for you: app-modules/ my-module/ composer.json + config/ + my-module.php src/ tests/ routes/ @@ -143,6 +145,7 @@ should work as expected in most cases: - Policies are auto-discovered for your Models - Blade components will be auto-discovered - Event listeners will be auto-discovered +- Config files are auto-registered (e.g. `config/my-module.php` → `config('my-module.*')`) ### Commands diff --git a/src/Console/Commands/Make/MakeModule.php b/src/Console/Commands/Make/MakeModule.php index 97bef56..a50acc4 100644 --- a/src/Console/Commands/Make/MakeModule.php +++ b/src/Console/Commands/Make/MakeModule.php @@ -275,6 +275,7 @@ protected function getStubs(): array return [ 'composer.json' => $this->pathToStub('composer-stub-latest.json'), 'src/Providers/StubClassNamePrefixServiceProvider.php' => $this->pathToStub('ServiceProvider.php'), + 'config/StubModuleName.php' => $this->pathToStub('config.php'), 'routes/StubModuleName-routes.php' => $this->pathToStub('web-routes.php'), 'resources/views/.gitkeep' => $this->pathToStub('.gitkeep'), 'database/factories/.gitkeep' => $this->pathToStub('.gitkeep'), diff --git a/src/Plugins/Attributes/HandlesRegister.php b/src/Plugins/Attributes/HandlesRegister.php new file mode 100644 index 0000000..3bc5161 --- /dev/null +++ b/src/Plugins/Attributes/HandlesRegister.php @@ -0,0 +1,11 @@ +registry->modules() + ->map(fn($module) => [ + 'key' => $module->name, + 'path' => $module->path("config/{$module->name}.php"), + ]) + ->filter(fn($row) => file_exists($row['path'])); + } + + public function handle(Collection $data): void + { + if ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached()) { + return; + } + + $config = $this->app->make('config'); + + $data->each(fn(array $row) => $config->set($row['key'], array_merge( + require $row['path'], + $config->get($row['key'], []), + ))); + } +} diff --git a/src/Plugins/Plugin.php b/src/Plugins/Plugin.php index b0d01ca..5fbcdd2 100644 --- a/src/Plugins/Plugin.php +++ b/src/Plugins/Plugin.php @@ -6,17 +6,32 @@ use Illuminate\Foundation\Application; use Illuminate\Support\Collection; use InterNACHI\Modular\Plugins\Attributes\HandlesBoot; +use InterNACHI\Modular\Plugins\Attributes\HandlesRegister; use InterNACHI\Modular\Support\FinderFactory; use ReflectionAttribute; use ReflectionClass; abstract class Plugin { + public static function register(Closure $handler, Application $app): void + { + static::firstRegisterableAttribute()?->newInstance()->register(static::class, $handler, $app); + } + public static function boot(Closure $handler, Application $app): void { static::firstBootableAttribute()?->newInstance()->boot(static::class, $handler, $app); } + /** @return ReflectionAttribute|null */ + protected static function firstRegisterableAttribute(): ?ReflectionAttribute + { + $attributes = (new ReflectionClass(static::class)) + ->getAttributes(HandlesRegister::class, ReflectionAttribute::IS_INSTANCEOF); + + return $attributes[0] ?? null; + } + /** @return ReflectionAttribute|null */ protected static function firstBootableAttribute(): ?ReflectionAttribute { diff --git a/src/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 33bf966..09de2bc 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -16,6 +16,7 @@ use InterNACHI\Modular\PluginRegistry; use InterNACHI\Modular\Plugins\ArtisanPlugin; use InterNACHI\Modular\Plugins\BladePlugin; +use InterNACHI\Modular\Plugins\ConfigPlugin; use InterNACHI\Modular\Plugins\EventsPlugin; use InterNACHI\Modular\Plugins\GatePlugin; use InterNACHI\Modular\Plugins\MigratorPlugin; @@ -67,6 +68,7 @@ public function register(): void // All plugins are singletons $this->app->singleton(ArtisanPlugin::class); $this->app->singleton(BladePlugin::class); + $this->app->singleton(ConfigPlugin::class); $this->app->singleton(EventsPlugin::class); $this->app->singleton(GatePlugin::class); $this->app->singleton(MigratorPlugin::class); @@ -82,6 +84,7 @@ public function register(): void $this->registerEloquentFactories(); $this->registerDefaultPlugins(); + $this->app->make(PluginHandler::class)->register($this->app); $this->app->booting(fn() => $this->app->make(PluginHandler::class)->boot($this->app)); } @@ -113,6 +116,7 @@ protected function registerDefaultPlugins(): void $registry->add( ArtisanPlugin::class, BladePlugin::class, + ConfigPlugin::class, EventsPlugin::class, GatePlugin::class, MigratorPlugin::class, diff --git a/src/Support/PluginHandler.php b/src/Support/PluginHandler.php index c7a0945..5988f90 100644 --- a/src/Support/PluginHandler.php +++ b/src/Support/PluginHandler.php @@ -14,6 +14,13 @@ public function __construct( ) { } + public function register(Application $app): void + { + foreach ($this->registry->all() as $class) { + $class::register($this->handle(...), $app); + } + } + public function boot(Application $app): void { foreach ($this->registry->all() as $class) { diff --git a/stubs/config.php b/stubs/config.php new file mode 100644 index 0000000..dcc7411 --- /dev/null +++ b/stubs/config.php @@ -0,0 +1,5 @@ +assertEquals('test_value', config('test-module.test_key')); + } + + public function test_nested_config_values_are_accessible(): void + { + $this->assertEquals('nested_value', config('test-module.nested.key')); + } +} diff --git a/tests/testbench-core/app-modules/test-module/config/test-module.php b/tests/testbench-core/app-modules/test-module/config/test-module.php new file mode 100644 index 0000000..049d080 --- /dev/null +++ b/tests/testbench-core/app-modules/test-module/config/test-module.php @@ -0,0 +1,8 @@ + 'test_value', + 'nested' => [ + 'key' => 'nested_value', + ], +];