Skip to content

Conversation

rsd
Copy link

@rsd rsd commented Sep 23, 2025

Why

Reusable modules need defaults while letting each app override. Today, when keys collide, module config wins.
Opt-in only; existing behavior unchanged unless providers call the new helper.

What

  • New Nwidart\Modules\Traits\ConfigMergerTrait
  • mergeConfigDefaultsFrom($path, $key, $existingPrecedence = true, $deep = true)
    • App (existing) config wins by default
    • Deep merge (default) via array_replace_recursive, or shallow via array_merge
    • No-op when config is cached

Usage (in a module ServiceProvider)

use Nwidart\Modules\Traits\ConfigMergerTrait;

class ModuleServiceProvider extends ServiceProvider
{
    use ConfigMergerTrait;

    protected function registerConfig(): void
    {
        $configPath = module_path($this->name, config('modules.paths.generator.config.path'));

        if (is_dir($configPath)) {
            $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));

            foreach ($iterator as $file) {
                if ($file->isFile() && $file->getExtension() === 'php') {
                    $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
                    $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
                    $segments = explode('.', $this->nameLower.'.'.$config_key);

                    // remove duplicated adjacent segments
                    $normalized = [];
                    foreach ($segments as $segment) {
                        if (end($normalized) !== $segment) $normalized[] = $segment;
                    }

                    $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);

                    $this->publishes([$file->getPathname() => config_path($config)], 'config');
                    $this->mergeConfigDefaultsFrom($file->getPathname(), $key); // app wins (deep) by default
                }
            }
        }
    }
}

@solomon-ochepa
Copy link
Contributor

When working with modular architectures like nWidart/laravel-modules, modules should always override the root app configuration because of the principle of contextual precedence.

In a modular system, the app provides the global foundation — default configurations, shared behaviors, and system-wide fallbacks. Each module, however, represents a self-contained and context-specific feature set. By design, a module should encapsulate and control its own logic, dependencies, and configurations without being constrained by the global layer.

Here’s why modules should “win” in case of conflicts:

  1. Encapsulation & Autonomy
    Each module is a self-contained package meant to function independently. Allowing the app to override its configuration defeats modular isolation and couples module behavior to the global app layer.

  2. Predictability
    When a module explicitly defines a configuration, it’s intentional and contextually bound to that module’s operation. Prioritizing module configs ensures consistent, predictable behavior within that boundary.

  3. Extensibility & Maintainability
    Developers can safely customize or extend a module without unintentionally breaking global app logic. Updates and versioning also become simpler since module behavior remains consistent across different projects.

  4. Override Hierarchy (Principle of Local Authority)
    In layered architectures, local configuration should always take precedence over global defaults. This is similar to CSS cascading, environment overrides, or Laravel’s own config:cache behavior where closer scope equals higher priority.

  5. Ease of Integration
    For SaaS or multi-tenant applications using modular structures, each module might require tenant- or feature-specific settings. Enforcing module-level precedence makes integration cleaner and avoids global interference.


In short:

The app defines the baseline; modules refine and override it for contextual correctness.
Therefore, when conflicts arise, the module should win, because it represents the final, context-specific definition of behavior.

@rsd
Copy link
Author

rsd commented Oct 9, 2025

I understand your point.
But for the sake of the argument, suppose you distribute a module to be installable, lets say with joshbrw/laravel-module-installer.
This way, the module probably have sane defaults in the config.

However, these sane defaults are not for every situation, so you might want to change that. So you publish the configs as usual:

php artisan vendor:publish --provider="Modules\Foo\FooServiceProvider" --tag="config"

After this, it is natural that the config published would have precedence over the one in the module, right?

Note that I am not proposing to change the default behavior. I am just giving an opportunity for the module author that wish this, to have the opportunity to edit its Service Provider.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants