[Coldbox 6.8.1] Is it Possible to Set Submodule Settings Within a Parent Module?

I am trying to think more modularly these days, and I was curious if it is possible to configure a module’s child module settings within ModuleConfig.cfc? My objective is to be able to have an apps modules provide defaults for any child module dependencies.

For example, lets say you have the following app/module hierarchy:

/ (root app) - configured with /config/coldbox.cfc
-- modules/loginModule <-- sub module with moduleConfig.cfc
--/modules/loginModule/modules/recaptcha <-- sub module dependency

I’d like to have the loginModule provide some type of default configuration for its dependency module, recaptcha, without the root app having to configure everything in /config/coldbox.cfc.

Of course, I still want the root app to be able to override the settings that loginModule sets to recaptcha, but it would be nice if a module could configure its own dependencies with some defaults.

I feel like the settings priority should be this:

  1. Root app config/coldbox.cfc (1st priority)
  2. Module app ModuleConfig (2nd priority)
  3. Sub Module app ModuleConfig (3rd priority)

Is this possible?

Yeah, but it’s not as encapsulated as I would like. Allowing more than one version of the same module to exist inside a ColdBox app is a nut we’ve never cracked, so regardless of how modules are nested on disk or whether they are declared as “dependencies” in ModuleConfig.cfc, all modules are loaded in the same global namespace and share the same top level moduleSettings override.

So basically, the same sort of

modulesSettings = {
  myModule : {
    mySetting : 'value'
  }
};

that you’d do in your App’s Coldbox.cfc would need to be done, but from your outer module’s ModuleConfig.cfc.

You may be tempted to set things into the parentSettings variable in your module’s configure() method, which will be “appended and override the host application settings”.

However, THIS DOES NOT WORK like you want as it does a complete and total override of the ENTIRE moduleSettings struct, blowing away all module settings set at the top level. Ugh, that part of ColdBox could use some help…

So, in order to do this you need to reach into ColdBox and directly override the module settings so we can be a bit more nuanced and not just around clobbering entire structs. This gets annoyingly complicated due to the fact you

  • can’t control the order modules are registered
  • even if you list a module as a dependency, that only controls the order they activate in, not the order they register in
  • the module settings are processed in registration
  • there is like 3 freaking places the stupid module settings can live in memory-- in the settings struct of the ModuleConfig instance, in the moduleSettings struct of the ColdBox.cfc instance, and in the modules.moduleName.settings struct of the ColdBox config struct.

Overriding the ColdBox config’s moduleSettings only works if the module in question hasn’t been registered yet. If it has, then you have to override the module’s settings in Coldbox’s config. This code here should handle both cases. Put this in the configure() method of the module that wants to set default setting for another module somewhere.

var moduleSettings = controller.getSetting( 'modules' );
// If module is already registered with ColdBOx, then add a setting directly
if( moduleSettings.keyExists( 'myModule' ) ) {
	moduleSettings.myModule.settings.anotherSetting = 'value';
} else {
	// Otherwise, add it to ColdBox.cfc's moduleSettings to be used when the module IS registered in the future
	var coldBoxConfig = controller.getSetting( "ColdBoxConfig" );
	coldBoxConfig.getVariablesMixin = wirebox.getUtility().getMixerUtil().getVariablesMixin;
	var configVars = coldBoxConfig.getVariablesMixin();
	// Don't assume this exists
	configVars.moduleSettings = configVars.moduleSettings ?: {};
	configVars.moduleSettings.myModule.anotherSetting = 'value2';
}

Hmm, I’m not entirely sure about that. You can always check to see if the variable exists before setting it, but the two halves of the if statement above operate on different assumptions. The second half which sets the ModuleSettings override in the ColdBox.cfc would work pretty well if you only set keys that didn’t exist (giving the root app first priority). However, the first half of the if statement is operating on the finalized list of settings as the module is already registered and the main app’s settings have already been merged with the module’s default settings. In that case, it wouldn’t quite be clear where the settings came from, unless you added some more code to inspect the ColdBox .cfc’s moduleSettings struct and tried to made a guess as to whether the root app had already attempted to override anything.

1 Like

Brad, this is awesome, thank you for taking the time to write a code example.

You read my mind. Just minutes ago I was trying to go about the parentSettings method you just described. I’m glad I caught your message while I still have hair left to pull out. :wink:

Regarding module configuration priority, in defense of your argument, we sometimes allow modules to override root app configurations. For example, with cbSecurity you can override the default config within a submodule to further refine security rules for that module. We wouldn’t want the root app to clear out all of the security rules, so your point is a good one!

On the other hand, take a generic CMS module, that should be configured with a bunch of defaults (background color, default mail config, etc). In an ideal world, the CMS module should be ready to use out of the box (pun intended) using its default configuration, with all child modules configured. Then, if the root app wants to make a customization, or break away from the defaults, they can override the settings using their /config/coldbox.cfc. Here’s an example:

// root app's config/coldbox.cfc
moduleSettings = {
    myCmsModule: {
        backgroundColor: "blue", // overriding the module's background color
        recaptcha: { // <-- child module settings that will be overridden
            publicKey: "12345",
            secretKey: "54321"
        }
    }
}

Then… in the myCmsModule’s ModuleConfig.cfc, we could use your code to establish defaults AFTER the module is loaded with any overrides. I tweaked the code you wrote and moved it to the postModuleLoad() method because we want to use any overridden configuration properties:

// ModuleConfig.cfc inside of myCmsModule
function configure(){ 

    variables.settings = {
        backgroundColor: "red",
        recaptcha = { <-- default child module configuration
            publicKey: "foo",
            secretKey: "bar"
        }
    }

}

function postModuleLoad( event, interceptData, buffer, rc, prc ) {

    var moduleSettings = controller.getSetting( 'modules' );
    // If module is already registered with ColdBox, then add a setting directly
    if( moduleSettings.keyExists( 'recaptcha' ) ) {
        moduleSettings.recaptcha.settings.publicKey = variables.settings.recaptcha.publicKey;
        moduleSettings.recaptcha.settings.secretKey = variables.settings.recaptcha.secretKey;
    } else {
        // Otherwise, add it to ColdBox.cfc's moduleSettings to be used when the module IS registered in the future
        var coldBoxConfig = controller.getSetting( "ColdBoxConfig" );
        coldBoxConfig.getVariablesMixin = wirebox.getUtility().getMixerUtil().getVariablesMixin;
        var configVars = coldBoxConfig.getVariablesMixin();
        // Don't assume this exists
        configVars.moduleSettings = configVars.moduleSettings ?: {};
        configVars.moduleSettings.recaptcha.publicKey = variables.settings.recaptcha.publicKey;
        configVars.moduleSettings.recaptcha.secretKey = variables.settings.recaptcha.secretKey;
    }

}

I’m still in brainstorming mode, so there may be problems with the above idea. However, I am thinking I could push this even further by creating a new method within ModuleConfig.cfc which would execute the code you wrote dynamically so each key doesn’t have to be written explicitly.

1 Like

I took things a step further:

I decided against using postModuleLoad() because it was firing every time a module was loaded. I switched instead to using onLoad().

I also created a new convention in my module’s configure() method which will contain any default child module settings. Here’s what it looks like:

// ModuleConfig.cfc
function configure(){ 

    // default settings for the module
    variables.settings = {
        backgroundColor: "red"
    }
    
    // default configuration for any child module
    variables.moduleSettings = {
        
        recaptcha = {
            publicKey: "foo",
            secretKey: "bar"
        }
    }

}

/**
 * Fired when the module is registered and activated.
 */
function onLoad(){

    var moduleSettings = controller.getSetting( 'modules' );

    for ( var module in variables.moduleSettings ) {
        
        // no guarantees that this exists yet.
        param name="variables.settings.#module#" default = {};
        
        // append the defaults without override
        variables.settings[ module ].append( variables.moduleSettings[ module ], false );
        
        if ( variables.settings.keyExists( module ) ) {
            
            if( moduleSettings.keyExists( module ) ) {
                for ( var key in variables.settings[ module ] ) {
                    moduleSettings[ module ].settings[ key ] = variables.settings[ module ][ key ];
                }
            } else {
                // Otherwise, add it to ColdBox.cfc's moduleSettings to be used when the module IS registered in the future
                var coldBoxConfig = controller.getSetting( "ColdBoxConfig" );
                coldBoxConfig.getVariablesMixin = wirebox.getUtility().getMixerUtil().getVariablesMixin;
                var configVars = coldBoxConfig.getVariablesMixin();
                configVars.moduleSettings = configVars.moduleSettings ?: {};

                for ( var key in variables.settings[ module ] ) {
                    configVars.moduleSettings[ module ][ key ] = variables.settings[ module ][ key ];
                }
            }

        }
    }
}

Then in your root app, you would have:

// coldbox.cfc
moduleSettings = {
    myCmsModule: {
        recaptcha = {
            secretKey = "root secret key"
        }
    }
};

The end result is that the recaptcha module is initialized with the defaults from myCmsModule, but the secretKey in the child module, recaptcha, is overriden with the value “root secret key”.

I couldn’t find a way to simulate the race condition where I need to use the secondary coldBoxConfig = controller.getSetting( "ColdBoxConfig" ) way of defaulting the configuration. Hopefully the fact that my module config looks good on subsequent refreshes is a good sign. I will continue to test with some more child modules.