From 9c6851aaf782288b3b84a9205652126e1f9a0255 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Fri, 13 Feb 2026 07:47:36 -0600 Subject: [PATCH 1/3] chore: add .editorconfig for consistent formatting This project uses tabs for PHP indentation. Adding an .editorconfig helps editors and IDEs respect this convention automatically. - PHP: tabs (project convention) - JSON: 4 spaces - YAML: 2 spaces - All files: UTF-8, LF line endings, final newline --- .editorconfig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .editorconfig 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 From 43f6d24190aa12fde779716bdad617fb23f302c6 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Tue, 10 Feb 2026 09:58:48 -0600 Subject: [PATCH 2/3] feat: add OnRegister attribute for plugins that run during registration Introduces a registration-phase hook for plugins, mirroring the existing boot-phase pattern. Plugins can use #[OnRegister] attribute to run during the service provider's register() method instead of boot(). This is needed for plugins like config loading that must complete before other services are resolved. --- CHANGELOG.md | 1 + CHANGES-V3.md | 12 ++++++++++-- src/Plugins/Attributes/HandlesRegister.php | 11 +++++++++++ src/Plugins/Attributes/OnRegister.php | 16 ++++++++++++++++ src/Plugins/Plugin.php | 15 +++++++++++++++ src/Support/ModularServiceProvider.php | 1 + src/Support/PluginHandler.php | 7 +++++++ 7 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/Plugins/Attributes/HandlesRegister.php create mode 100644 src/Plugins/Attributes/OnRegister.php diff --git a/CHANGELOG.md b/CHANGELOG.md index df358fc..f9857a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 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 ## [2.2.0] - 2024-04-05 diff --git a/CHANGES-V3.md b/CHANGES-V3.md index 3d1c331..92d5640 100644 --- a/CHANGES-V3.md +++ b/CHANGES-V3.md @@ -56,10 +56,15 @@ 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 | @@ -74,14 +79,17 @@ abstract class Plugin | `GatePlugin` | `AfterResolving(Gate)` | Register model policies | | `ArtisanPlugin` | `Artisan::starting()` | Register commands | -## Boot Flow +## 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/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 @@ +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..26557c0 100644 --- a/src/Support/ModularServiceProvider.php +++ b/src/Support/ModularServiceProvider.php @@ -82,6 +82,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)); } 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) { From 1443de993900647bf4dabdd62b72d086e7391dbc Mon Sep 17 00:00:00 2001 From: Caleb White Date: Tue, 10 Feb 2026 09:58:48 -0600 Subject: [PATCH 3/3] feat: add ConfigPlugin to auto-load module config files Add ConfigPlugin that automatically discovers and loads configuration files from module config/ directories. Module configs are merged with app-level configs, where app-level config values take precedence over module defaults. --- CHANGELOG.md | 2 + CHANGES-V3.md | 18 ++++++++ README.md | 3 ++ src/Console/Commands/Make/MakeModule.php | 1 + src/Plugins/ConfigPlugin.php | 44 +++++++++++++++++++ src/Support/ModularServiceProvider.php | 3 ++ stubs/config.php | 5 +++ tests/Plugins/ConfigPluginTest.php | 21 +++++++++ .../test-module/config/test-module.php | 8 ++++ 9 files changed, 105 insertions(+) create mode 100644 src/Plugins/ConfigPlugin.php create mode 100644 stubs/config.php create mode 100644 tests/Plugins/ConfigPluginTest.php create mode 100644 tests/testbench-core/app-modules/test-module/config/test-module.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f9857a8..99aa733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 92d5640..6aba50e 100644 --- a/CHANGES-V3.md +++ b/CHANGES-V3.md @@ -70,6 +70,7 @@ services boot. | 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 | @@ -79,6 +80,23 @@ services boot. | `GatePlugin` | `AfterResolving(Gate)` | Register model policies | | `ArtisanPlugin` | `Artisan::starting()` | Register commands | +### 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 ``` 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/ConfigPlugin.php b/src/Plugins/ConfigPlugin.php new file mode 100644 index 0000000..c2d02c1 --- /dev/null +++ b/src/Plugins/ConfigPlugin.php @@ -0,0 +1,44 @@ +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/Support/ModularServiceProvider.php b/src/Support/ModularServiceProvider.php index 26557c0..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); @@ -114,6 +116,7 @@ protected function registerDefaultPlugins(): void $registry->add( ArtisanPlugin::class, BladePlugin::class, + ConfigPlugin::class, EventsPlugin::class, GatePlugin::class, MigratorPlugin::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', + ], +];