Multiple Configuration Files with includes and modules - Mach-II/Mach-II-Framework GitHub Wiki
Original Specs by Kurt Wiersma (kurt@…)
Updated by Kurt Wiersma (kurt@…) and Peter J. Farrell (peter@…)
- The Problem
- Exploring the Solutions
-
The Solution -
<includes>
-
The Solution -
<modules>
- Module Inheritance
- Module Loading
- Upgraded Commands
- In Closing
- References
When building large Mach-II applications, developers often must deal with a large XML configuration file. Configuration files of over 1,000 lines are not uncommon. This makes it difficult to navigate the document to add or edit views and plug-ins as well as navigate directly to a specific event. Editors like Eclipse have XML plug-ins that can assist somewhat in the navigation but ultimately a large file slows down developers. Furthermore, development teams often are editing the same Mach-II configuration file at the same time as other team members creating the possibility of conflicts. Finally, when building applications with several discrete areas or modules it might be more efficient to be able to develop and reload the configuration of those modules separately.
When building a large application is it useful to be able to develop the application in a modular fashion. One example might be in an online store administration interface which could contain a product and orders administration interface. A set of shared properties and listeners could be developed and then one developer could implement the order management module while another developer could implement the product management module.
Several people have suggested using XML entities to include another file in the main configuration file. This solution works but unfortunately, due to how ColdFusion's XML parser is setup, you must provide the full path to the file you want to want include. Production servers often have different paths then the development teams machines making this solution difficult to use. As a result of this, multiple people have proposed adding an include tag to the configuration file in order to support loading a file relative to the Mach-II application root. Also, any changes to config files that included in this manner are picked up when the machii_config_mode = 0
(dynamic) which can cause a developer to waste time debugging when all that is needed is to reload the entire application.
Having include file support helps developers break up the configuration into logical chunks but doesn’t completely address building applications in a more modular fashion. What is needed is a way to group together all the items needed for a particular module of an application. These pieces include: views, listeners, event-handlers, plug-ins, properties, and filters.
The first feature that Mach-II needs is the ability to include an external XML config file. The include tag allows files to be included relative to the root of the Mach-II application or ColdFusion mapping.
<mach-ii>
<includes>
<include file="config/additional-stuff.xml" />
</includes>
. . . additional elements . . .
</mach-ii>
As you can see above, the new include tag must be defined in the <includes>
section of the Mach-II configuration file. This section is now the first section in the root node of <mach-ii>
. Include files must be a valid Mach-II configuration file and contain a root node of <mach-ii>
. Any other elements defined in an include file are optional. We can foresee developers using includes to define default properties across applications, or to encapsulate event-handlers and view-page declares for an admin section of a website. Includes cannot have duplicate event-handlers, listeners, filters or plugins as the framework will thrown an "already defined" error. For the ability to override, extend and modularize sections of code into free-standing sub-applications, please read about modules.
The path for the file
attribute of <include>
takes a path with a CFML mapping or a relative path (which is relative from the path of the file that declares the include). We added relative paths to make integration with third-party modules (such as MachBlog) easier.
Everybody knows the ../
(move up a directory) and ./
(same directory) syntax from HTML and *nix. Relative path notation for Mach-II uses the same syntax. All relative paths are relative from the configuration file that defines the path to the other configuration file - not relative to your webroot. For example, if you have a config file named mach-ii.xml
in a directory named /appRoot/config/
and you want to include another config file that is located in the same directory as your mach-ii.xml
file, it would look like this:
<include file="./other-mach-ii.xml"/>
However, you cannot just use other-mach-ii.xml
as the path since CFML's ExpandPath
will try to expand from the webroot. You must start a relative path with the ./
or ../
syntax - which indicates to Mach-II that you are using a relativat path.
Keeping in the same style, if you have your mach-ii.xml
file in the /appRoot/config/
and you want to define a include that lives in /appRoot/includes/anotherDir/config/mach-ii.xml
, it would look like this:
<module name="blog" file="../includes/anotherDir/config/mach-ii.xml"/>
The example above uses the ../
syntax to move up a directory (from the /appRoot/config/
), into the /appRoot
directory and then in the directory path specified.
All include files must have a root node of <mach-ii>
just like all other Mach-II XML configuration files. Other child nodes are optional (meaning you can have includes with just properties or a mix of nodes like listeners / plugins - it is not required that all nodes are present).
Incorrect Syntax:
<properties>
<property name="foo" value="1"/>
<property name="bar" value="2"/>
</properties>
Correct Syntax:
<mach-ii>
<properties>
<property name="foo" value="1"/>
<property name="bar" value="2"/>
</properties>
</mach-ii>
-
<includes>
are available in Mach-II 1.5 or above. - All includes are checked for changes when the 'machii_config_mode
is set to 0 (dynamic). There is some overhead related to checking for changes to include files. Team Mach-II always recommends that you run your application with
machii_config_mode` in -1 (never) when deploying your application to production environments. - The override attribute of the
<include>
tag defaults to "false". This means if your include file has listeners, filters, modules, event-handlers, subroutines or plugin elements that conflict with elements in the base config file, Mach-II will throw an exception about the duplicate element. Depending on how your application is designed, you may need to set the override attribute to "true" which causes any conflicting elements in the base config file to be overridden with the elements from the include config file. - The file attribute of the
<include>
tag supports relative paths. We use the../
(move up a directory) and./
(same directory) syntax from HTML and *nix. All relative paths must start with./
or../
All relative paths are relative from the configuration file that defines the path to the other configuration file - not relative to your webroot or application root.
The second new feature that will be implemented is support for modules. To maintain backwards compatibility, modules can have their own new section of the configuration file right after the includes section and before the properties section. All of the items defined in the base of the configuration file could be put into the "default" module, which could be overridden by any other modules. Modules can also extend one other module. By extending a module you can override specific event-handlers, views, or listener definitions. Note that the configuration file is processed sequentially down the document, so the module referenced by an "extends" should be defined above the module which extends it.
<mach-ii version="1.5">
<includes>
<!--
This include contains all the event-handlers and
page-views for our products section of the website
-->
<include file="config/mach-ii_products.xml" />
</includes>
<modules>
<module name="blog" file="modules/machblog/config/mach-ii.xml.cfm">
<mach-ii version="1.5">
<properties>
<property name="dsn" value="myBlogDsn"/>
</properties>
<listeners>
. . .
</listeners>
. . .
</mach-ii>
</module>
<module name="blogAddon" extends="blog" />
</modules>
<properties>
. . .
</properties>
. . . additional elements . . .
</mach-ii>
Inside a module configuration file, specified by the "file" attribute, you would have a <mach-ii>
tag followed by all the normal configuration tags like properties, listeners, filters, views, and plug-ins. Any properties, listeners, and views with the same name as a property in the module that was extended will be overridden by the sub-module. Allowing you to setup properties and other elements would allow you to override settings in the module file.
Also, you can override properties, listeners, filers, views, and plug-ins by defining them between the <module>
tag. For instance, you could define the open-source MachBlog application as a module and override the DSN property in the MachBlog configuration file within the blog's <module>
tag. This will allow you consistently use the BER of MachBlog without ever having to edit that configuration file. This overriding feature is designed to allow easy and continuous integration of third-party sub-applications without modifying that module's configuration file.
When declaring the module, you can also include events from the base as if they were declared in the module itself. For example:
<module name="module1" file="/modules/module1/config/mach-ii.xml">
<mach-ii>
<event-handlers>
<event-handler event="common-event" overrideAction="addFromParent" />
</event-handlers>
</mach-ii>
</module>
By supplying 'overrideAction=addFromParent', the event is effectively copied into the module declaration. This allows you to inject events into the module without the need to modify the module's configuration. It also gives the benefit of the event running in the module's scope. In this case, calling event.getModuleName()
will return 'module1'.
Alternatively, one could declare 'common-event' in the module1 config like:
<event-handler event="common-event">
<announce event="common-event" module="" />
</event-handler>
However, in this case, the event would run in the scope of the base module. This may be desired functionationaly, but it is important to understand the difference.
Additionally, if you wish to copy the event to the module, but give it a different name, this can be accomplished with the 'mapping' attribute:
<module name="module1" file="/modules/module1/config/mach-ii.xml">
<mach-ii>
<event-handlers>
<event-handler event="my-module-event" overrideAction="addFromParent" mapping="common-event" />
</event-handlers>
</mach-ii>
</module>
Now, by calling module1:my-module-event, the base event 'common-event' is executed, but still within the context of module1.
The 'useParent' value works in a similar way, but instead of including an event from the parent, it prevents the execution of the event in the module and forces a call to base event. Consider the following module configuration:
<event-handlers>
<event-handler event="common-event" access="public" >
<view-page name="module-view" />
</event-handler>
<event-handler event="my-event" access="public" >
<announce event="common-event" />
</event-handler>
</event-handlers>
This is a very straight-forward declaration of 'my-event' announcing the module's 'common-event' event causing 'module-view' do be displayed. However, by adding the following to the base configuration:
<event-handlers>
<event-handler event="common-event" access="public" />
<view-page name="base-view" />
</event-handler>
</event-handlers>
<module name="module1" file="/modules/module1/config/mach-ii.xml">
<mach-ii>
<event-handlers>
<event-handler event="common-event" overrideAction="useParent" />
</event-handlers>
</mach-ii>
</module>
This will force a call to module1:my-event to announce 'common-event' in the *base* configuration causing 'base-view' to be displayed. You cannot call module1:comment-event via the URL, if you do so you will receive an error, something like: "Event-handler for event 'common-event' in module 'Module1' is not defined". This is by design, to prevent announcing an inherited event-handler in the context of the module. So overridden event-handlers can only be called in a module programmatically (by another event-handler, with announceEvent(), etc).
It is also worth noting that includes files may also be used as a convenience when overriding module functionality. If you have a list of common events you want to override in a module, they can be defined in a seperate XML file and included. For example:
<module name="module1" file="/modules/module1/config/mach-ii.xml">
<include file="common-events.xml" />
</module>
<module name="module2" file="/modules/module1/config/mach-ii.xml">
<include file="common-events.xml" />
</module>
The implementation of modules requires a change to how events will look in the url. Since the base/main configuration file could have an event named showLoginForm
and a module that does not override the parent configuration file could have an event named showLoginForm
, we would not know which event-handler to call. Thus any module events need to be prefixed with a module name mapping.
Calling the event in the base/main:
http://www.example.com/index.cfm?event=showLoginForm
Calling the event in the module:
http://www.example.com/index.cfm?event=blog:showLoginForm
The absence of a module in the event name denotes that the event-handler to be executed exists in the base/main configuration file. With the addition of the BuildUrl()
and BuildUrlToModule()
utility methods, building hyperlinks in the correct moduleName:eventName
syntax will be easy. We will add a new property called moduleDelimiter
as optional property and allows you to choose the module delimiter of your choice (could be "|" or a "," etc.). However, your event names cannot have module delimiter in them. For example, you could not use :
as a module delimiter if you typical construct your event names with :
in them since it conflicts with the module delimiter (which needs to be unique).
Since all modules inherit from the base application, certain properties for the framework set in the base application are inherited and cannot be overridden by modules. This includes properties that drive URL writing behavior (think buildUrl()
) and things like the eventParameter
. This is because the framework is a front controller framework and is looking for specific "clues" in the incoming URLs in order to announce the requested event-handler. If the incoming URLs use two different formats (such as differing eventParameters
values in a module and base application), the framework would not be able to decide what to do with the request.
It's worth noting that modules do inherit views from the base. If an event-handler is executed within the context of a module, it will first look for a given view-page first within the module, then if it does not exist, it will look in the base, and finally, it will error out. So in a sense, if you're executing an event in the module, the module's views take precedence. This also applies for listeners, event-filters and subroutines.
This could become an issue if you use the same view-page name in both your base and module, and then try to override a module's event-handler. It will use the base event-handler and then look for the view page; and since it will check the module first, it will use the module's view page. Of course, this can be easily avoided by using some sort of naming convention to avoid name collisions.
The following properties cannot be overridden in a module and will inherit from the base application:
eventParameter
parameterPrecedence
maxEvents
redirectPersistParameter
redirectPersistScope
redirectPersistParameterLocation
moduleDelimiter
urlBase
urlDelimiters
urlParseSES
urlExcludeEventParameter
As of version 1.9, Mach-ii provides control over how modules are loaded and what to do when a module fails to load.
The following properties below can be defined. It is recommended that these be defined within an environment property definition.
Property | Options | Description |
---|---|---|
modules:disable | <module list> | Upon loading the framework, the supplied list modules will be disabled in the system. If one of the disabled modules is requested an execption of type "MachII.framework.ModuleDisabled " will be thrown. |
modules:lazyLoad | [ * | !* |
modules:disableOnFailure | [true | false] |
We have upgraded all commands that deal with announcing events by adding a new optional attribute named moduleName - <announce>
, <redirect>
and <event-mapping>
. Also, a new method called announceEventInModule()
was added to the MachII.framework.BaseComponent
and is available to all listeners, filters and plugins so you can announce events in a module.
With the addition of modules, the announce command has gotten upgrades to handle modules.
This method call has not changed in Mach-II 1.5
This method call as been added to announce an event in another module and can be used in any place that announceEvent()
can be used. Example:
<cfset announceEventInModule("nameOfModule", "nameOfEvent", structOfEventArgs) />
The announce command has added the optional "module" attribute. To announce an event in another module:
<announce module="someOtherModuleName" event="eventInSomeOtherModule" copyEventArgs="true|false"/>
The event-mapping command has added the optional mappingModule
attribute. To map to an event in another module:
<event-mapping event="pass" mapping="someCoolEvent" mappingModule="anotherModule"/>
- All modules and any of their includes are checked for changes when the
machii_config_mode
is set to 0 (dynamic). There is some overhead related to checking for changes to module files. Team Mach-II always recommends that you run your application withmachii_config_mode
in -1 (never) when deploying your application to production environments. - Modules can be programmatically reloaded. From any Mach-II component (like a Listener), call
<cfset getAppManager().getModuleManager().getModule("nameOfYourModule").reloadModuleConfig() />
. - Due to the complexity of the parent/child relationships.
Modules applications cannot have their own modules (i.e. nest a module within a module). - The file attribute of the
<module>
tag supports relative paths. We use the../
(move up a directory) and./
(same directory) syntax from HTML and *nix. All relative paths must start with./
or../
. All relative paths are relative from the configuration file that defines the path to the other configuration file - not relative to your webroot or application root.
The new include tag coupled with the new module support make it easier for Mach II developers to develop larger applications and/or integrate third party sub-applications. Backwards compatibility remains an important goal of the Mach-II team so we have attempted to develop these new features without disrupting current applications.