Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 28 additions & 2 deletions CHANGES-V3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
```
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/Make/MakeModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
11 changes: 11 additions & 0 deletions src/Plugins/Attributes/HandlesRegister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace InterNACHI\Modular\Plugins\Attributes;

use Closure;
use Illuminate\Foundation\Application;

interface HandlesRegister
{
public function register(string $plugin, Closure $handler, Application $app);
}
16 changes: 16 additions & 0 deletions src/Plugins/Attributes/OnRegister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace InterNACHI\Modular\Plugins\Attributes;

use Attribute;
use Closure;
use Illuminate\Foundation\Application;

#[Attribute(Attribute::TARGET_CLASS)]
class OnRegister implements HandlesRegister
{
public function register(string $plugin, Closure $handler, Application $app)
{
$handler($plugin);
}
}
44 changes: 44 additions & 0 deletions src/Plugins/ConfigPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace InterNACHI\Modular\Plugins;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesConfiguration;
use Illuminate\Support\Collection;
use InterNACHI\Modular\Plugins\Attributes\OnRegister;
use InterNACHI\Modular\Support\FinderFactory;
use InterNACHI\Modular\Support\ModuleRegistry;

#[OnRegister]
class ConfigPlugin extends Plugin
{
public function __construct(
protected Application $app,
protected ModuleRegistry $registry
) {
}

public function discover(FinderFactory $finders): iterable
{
return $this->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'], []),
)));
}
}
15 changes: 15 additions & 0 deletions src/Plugins/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<HandlesRegister>|null */
protected static function firstRegisterableAttribute(): ?ReflectionAttribute
{
$attributes = (new ReflectionClass(static::class))
->getAttributes(HandlesRegister::class, ReflectionAttribute::IS_INSTANCEOF);

return $attributes[0] ?? null;
}

/** @return ReflectionAttribute<HandlesBoot>|null */
protected static function firstBootableAttribute(): ?ReflectionAttribute
{
Expand Down
4 changes: 4 additions & 0 deletions src/Support/ModularServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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));
}
Expand Down Expand Up @@ -113,6 +116,7 @@ protected function registerDefaultPlugins(): void
$registry->add(
ArtisanPlugin::class,
BladePlugin::class,
ConfigPlugin::class,
EventsPlugin::class,
GatePlugin::class,
MigratorPlugin::class,
Expand Down
7 changes: 7 additions & 0 deletions src/Support/PluginHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions stubs/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

return [
//
];
21 changes: 21 additions & 0 deletions tests/Plugins/ConfigPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace InterNACHI\Modular\Tests\Plugins;

use InterNACHI\Modular\Tests\Concerns\PreloadsAppModules;
use InterNACHI\Modular\Tests\TestCase;

class ConfigPluginTest extends TestCase
{
use PreloadsAppModules;

public function test_module_config_is_loaded(): void
{
$this->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'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

return [
'test_key' => 'test_value',
'nested' => [
'key' => 'nested_value',
],
];