Blog

CommandBox - Commands can be Clean Code Too

Gavin Pickin February 17, 2015

Spread the word

Gavin Pickin

February 17, 2015

Spread the word


Share your thoughts

In today's world, we use frameworks, and separate concerns with MVC or MV*, we strive for Clean Code to make Uncle Bob proud, or Bossman Bob at least. Just because the “Run” function is all you need in a CFC (which extends the BaseCommand) for CommandBox to index the Command, doesn’t mean you have to jam all your code into that one function. Lets look at some simple code cleanup.

On my own blog, we've looked a creating a simple set of commands, which I decided to call KIWI SAYS, which help with creating Host files in Apache, Lucee, and gives you some shortcuts for restarting your engines, but the main command in the set is addWebsite. It is the main controller almost, that does a lot of checking config, and setup, and if everything is good, then we’ll actually do produce the output, and restart apache. It seems like a perfect place to start, because that's a lot of code.

Assuming I had done no cleanup or refactoring, my run function would look something like this.

component extends="commandbox.system.BaseCommand" {
   
    property name="settingsObj";
        
    /**
* @websitePath.hint Path to the Website Directory - .\ for current directory
* @websiteURL.hint The Website URL
* @engine.hint The CFML Engine you want the Website to run on - lucee, railo or cf11
* @engine.options Lucee,Railo,CF11
*/
    function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath =  fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
        if( !directoryExists( ARGUMENTS.websitePath ) ) {
            return error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
        }
        if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
               error('Please select a valid Engine - Lucee, Railo or CF11’);
        }
        var apacheConf = "";
        apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
        if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
            apacheConf = apacheConf & 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
        }
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
        apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
        if ( engine == 'CF11'){
            apacheConf = apacheConf & & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
        }
        else {
            apacheConf = apacheConf  & '<Proxy *>' & CR;
            apacheConf = apacheConf & 'Allow from 127.0.0.1' & CR;
            apacheConf = apacheConf & '</Proxy>' & CR;
            apacheConf = apacheConf & 'ProxyPreserveHost On' & CR;
            apacheConf = apacheConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';

            if ( engine == "Railo" ){
                  apacheConf = apacheConf & this.settingsObj.get("railoAJPPort");
            }
            else {
                  apacheConf = apacheConf & this.settingsObj.get("luceeAJPPort");
            }
            apacheConf = apacheConf &  replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
            apacheConf = apacheConf & '/$1$2' & CR;
        }
        apacheConf = apacheConf & '</VirtualHost>' & CR;
        print.line().blueLine( apacheConf );
        
        var apacheConfPath = this.settingsObj.get("apacheConfPath");
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( apacheConfPath == '' ) {
            return error( "#apacheConfPath#: No such directory" );
        }
   
        apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( !directoryExists( apacheConfPath ) ) {
            return error( "#apacheConfPath#: No such directory" );
        }
        print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
        fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
        
         runCommand( "kiwiSays restartApache" );
        
    }
}

 

Can you follow that code?

I wrote it, and to be honest, in this format, its not pretty, and I would not like working on this. Lets make it CLEAN(er) code… and we’ll do that like we would do with any other CFC, refactor a piece out into smaller functions. CommandBox doesn’t limited the number of functions you can have in a CFC, it will only index, and run the “RUN” function when the name of the CFC is typed into the console. Lets break this massive run function into a more management controller.

Note: I could actually add additional CFCs, like Services, and call each one to perform functions, but to make it easier for readers, all the functions I refactor are going into the same main CFC. The only exception, if my settingsObj.cfc which I use to wrap input and output to a json file, to store settings for this suite of tools… so you can set your paths for where CF is installed, where Lucee is installed, what ports etc.

First piece of code

BEFORE:

ARGUMENTS.websitePath =  fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
if( !directoryExists( ARGUMENTS.websitePath ) ) {
        return error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
}

We are validating the WebsitePath, so that sounds like a great function name.

AFTER:

ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );


NEW FUNCTION:

function validateWebsitePath( websitePath ){
   
    websitePath = fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
    if( !directoryExists( ARGUMENTS.websitePath ) ) { 
        error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
    }
    return websitePath;
   
}



Second piece of code

BEFORE:

if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
        error('Please select a valid Engine - Lucee, Railo or CF11’);
}

We are validating required settings, again a great name, so we’re going to replace this with a new function. The best thing about this, I have a lot more settings I want to validate, so I’m going to add more here, and we do not need to worry about the main function getting more polluted.

AFTER:

validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );

 

NEW FUNCTION:

function validateRequiredSettings( required string websiteURL, required string websitePath, required string engine ){
   
    if ( validateEngine( ARGUMENTS.engine ) != ""){
        return error( validateEngine( ARGUMENTS.engine ) );
   
    }
    return "";
   
}

 

Third piece of code

So far, we only saved ourselves a few lines, the next chunk of code, is where you see the real benefits.

BEFORE:

var apacheConf = "";
   
apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
    apacheConf = apacheConf & 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
}
apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
if ( engine == 'CF11'){
    apacheConf = apacheConf & & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
}
else {
    apacheConf = apacheConf  & '<Proxy *>' & CR;
    apacheConf = apacheConf & 'Allow from 127.0.0.1' & CR;
    apacheConf = apacheConf & '</Proxy>' & CR;
    apacheConf = apacheConf & 'ProxyPreserveHost On' & CR;
    apacheConf = apacheConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
    if ( engine == "Railo" ){
        apacheConf = apacheConf & this.settingsObj.get("railoAJPPort");
    }
    else {
        apacheConf = apacheConf & this.settingsObj.get("luceeAJPPort");
    }
    apacheConf = apacheConf &  replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" ) & '/$1$2' & CR;
}
apacheConf = apacheConf & '</VirtualHost>' & CR;
print.line().blueLine( apacheConf );

 

We are generating Apache conf, essentially, although this one piece of code does a lot of smaller tasks inside of it… so we’re first going to refactor it into 1 function, called generateApacheConf surprisingly enough… then we can refactor those other pieces out again.

AFTER:

var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );

Only 1 line, now our Run function is looking much better. Lets look at the generateApacheConf function.

NEW FUNCTION:

function generateApacheConf( required string websiteURL, required string websitePath, required string engine ){
   
    var apacheConf = "";
   
    apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
    apacheConf = apacheConf & getServerAdmin();
    apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
    apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
    apacheConf = apacheConf & getEngineConnection( engine, websitePath );
    apacheConf = apacheConf & '</VirtualHost>' & CR;

    print.line().blueLine( apacheConf );

    return apacheConf;
   
}

Wait, that's not the same code, no, I took the LOGIC out of this function too. Appending to the ApacheConf variable is easy, but if there was a decision to be made, or more complex output, I refactored that out too.


function getServerAdmin(){
    if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
        return 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
    }
    else {
        return '';
    }
}

This getServerAdmin function hides the task of determining if we need a ServerAdmin line in our config, and how to output it from our settings.

 

function getEngineConnection( engine, websitePath ){
    var returnConf = '';
   
    if ( engine == 'CF11'){
        returnConf = returnConf & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
    }
    else {
        returnConf = returnConf & '<Proxy *>' & CR;
        returnConf = returnConf & 'Allow from 127.0.0.1' & CR;
        returnConf = returnConf & '</Proxy>' & CR;
        returnConf = returnConf & 'ProxyPreserveHost On' & CR;
        returnConf = returnConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
        returnConf = returnConf & getEnginePort( engine );
        returnConf = returnConf & getProxyPath( websitePath ) & '/$1$2' & CR;

    }
    return returnConf;
}

getEngineConnection has a lot of complexity, and that too actually has some refactored functions too. The sub functions hide the complexity from the parent functions… so you can quickly and easily determine what the code is doing. The function names are self commenting, and if there is anything that doesn’t make sense, or is doing too much, I pull it out into its own function. getEnginePort and getProxyPath are both helper functions, but they make the getEngineConnection function much easier to read.

function getEnginePort( engine ){
    if ( engine == "Railo" ){
        return this.settingsObj.get("railoAJPPort");
    }
    else {
        return this.settingsObj.get("luceeAJPPort");
    }
}

function getProxyPath( required string websitePath ) {
    return replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
}



Last piece of Code

BEFORE:

var apacheConfPath = this.settingsObj.get("apacheConfPath");
print.line( 'apacheConfPath = ' & apacheConfPath );
if( apacheConfPath == '' ) {
    return error( "#apacheConfPath#: No such directory" );
}
   
apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
print.line( 'apacheConfPath = ' & apacheConfPath );
if( !directoryExists( apacheConfPath ) ) {
    return error( "#apacheConfPath#: No such directory" );
}
print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );

Which outputs the Conf to a File so we’ll make a function called outputConfToFile. 

NEW FUNCTION:

function outputConfToFile( required string apacheConf, required string websiteURL ){
    var apacheConfPath = this.settingsObj.get("apacheConfPath");
    print.line( 'apacheConfPath = ' & apacheConfPath );
    if( apacheConfPath == '' ) {
        return error( "#apacheConfPath#: No such directory" );
    }
   
    apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
    print.line( 'apacheConfPath = ' & apacheConfPath );
    if( !directoryExists( apacheConfPath ) ) {
        return error( "#apacheConfPath#: No such directory" );
    }
    print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
    fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
}

Looking at this function, I already want to break it up into multiple functions, but for now I will leave it as one function… as we have done a lot of work for one post.
Lets look at our main run function now.

function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );
        validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
       outputConfToFile( apacheConf, websiteURL );
       runCommand( "kiwiSays restartApache" );
}


Sharp, clean, simple, and to the point.
If you need to know what it does, you can see that from the code, if you want to learn more, you can peel back a layer.
Writing this, I can already see what could or should be broken down more… which is a good sign I’m on the right path.
Just because you are writing a command, doesn’t mean it cannot be clean code.

Full File is here for reference:

component extends="commandbox.system.BaseCommand" {
   
    property name="settingsObj";
        
    /**
    * @websitePath.hint Path to the Website Directory - .\ for current directory  
    * @websiteURL.hint The Website URL
    * @engine.hint The CFML Engine you want the Website to run on - lucee, railo or cf11
    * @engine.options Lucee,Railo,CF11
    */
    function run( required string websitePath, required string websiteURL, required string engine ){
        this.settingsObj = createObject( "component", "set.settings");
       
        ARGUMENTS.websitePath = validateWebsitePath( ARGUMENTS.websitePath );
        validateRequiredSettings( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        var apacheConf = generateApacheConf( ARGUMENTS.websiteURL, ARGUMENTS.websitePath, ARGUMENTS.engine );
        outputConfToFile( apacheConf, websiteURL );
        runCommand( "kiwiSays restartApache" );
    }
   
    function validateWebsitePath( websitePath ){
        websitePath = fileSystemUtil.resolvePath( ARGUMENTS.websitePath );
        if( !directoryExists( ARGUMENTS.websitePath ) ) {
             error( "#ARGUMENTS.websitePath#: No such directory - Please enter a valid relative or full path" );
        }
        return websitePath;  
    }
   
    function validateRequiredSettings( required string websiteURL, required string websitePath, required string engine ){
        if ( validateEngine( ARGUMENTS.engine ) != ""){
            return error( validateEngine( ARGUMENTS.engine ) );
        }
        return "";
    }
   
    function validateEngine( engine ){
        if ( listFind( "lucee,railo,cf11", ARGUMENTS.engine) == false ){
            return 'Please select a valid Engine - Lucee, Railo or CF11';
        }
    }
   
    function generateApacheConf( required string websiteURL, required string websitePath, required string engine ){
        var apacheConf = "";
   
        apacheConf = apacheConf & '<VirtualHost *:80>' & CR;
        apacheConf = apacheConf & getServerAdmin();
        apacheConf = apacheConf & 'DocumentRoot "#websitePath#"' & CR;
        apacheConf = apacheConf & 'ServerName #websiteURL#' & CR;
        apacheConf = apacheConf & getEngineConnection( engine, websitePath );
        apacheConf = apacheConf & '</VirtualHost>' & CR;

        print.line().blueLine( apacheConf );
        return apacheConf;
    }
   
    function outputConfToFile( required string apacheConf, required string websiteURL ){
       var apacheConfPath = this.settingsObj.get("apacheConfPath");
       print.line( 'apacheConfPath = ' & apacheConfPath );
       if( apacheConfPath == '' ) {
           return error( "#apacheConfPath#: No such directory" );
       }
       apacheConfPath = fileSystemUtil.resolvePath( apacheConfPath );
        print.line( 'apacheConfPath = ' & apacheConfPath );
        if( !directoryExists( apacheConfPath ) ) {
             return error( "#apacheConfPath#: No such directory" );
        }
        print.line( 'Writing Conf to File: ' & apacheConfPath & '/' & websiteURL & '.conf' );
        fileWrite( apacheConfPath & '/' & websiteURL & '.conf', apacheConf );
    }
   
    function getEngineConnection( engine, websitePath ){
       var returnConf = '';
        if ( engine == 'CF11'){
            returnConf = returnConf & 'Include "' & this.settingsObj.get("cf11ConnFile") & '"' & CR;
        }
        else {
            returnConf = returnConf & '<Proxy *>' & CR;
            returnConf = returnConf & 'Allow from 127.0.0.1' & CR;
            returnConf = returnConf & '</Proxy>' & CR;
            returnConf = returnConf & 'ProxyPreserveHost On' & CR;
            returnConf = returnConf & 'ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:';
            returnConf = returnConf & getEnginePort( engine );
            returnConf = returnConf & getProxyPath( websitePath ) & '/$1$2' & CR;
        }
        return returnConf;
    }
   
    function getEnginePort( engine ){
        if ( engine == "Railo" ){
            return this.settingsObj.get("railoAJPPort");
        }
        else {
            return this.settingsObj.get("luceeAJPPort");
        }
    }
   
    function getServerAdmin(){
        if ( this.settingsObj.get("ApacheAdminEmail") GT ""){
            return 'ServerAdmin ' & this.settingsObj.get("ApacheAdminEmail") & CR;
        }
        else {
            return '';
        }
    }
   
    function getProxyPath( required string websitePath ) {
        return replacenocase( websitePath, this.settingsObj.get("tomcatRootPath"), "" );
    }
}

 

Add Your Comment

Recent Entries

Ortus Solutions Brings Innovation with BoxLang as Platinum Sponsor and Keynote Presenter at CFCamp 2025!

Ortus Solutions Brings Innovation with BoxLang as Platinum Sponsor and Keynote Presenter at CFCamp 2025!

Join us for groundbreaking workshops and discover the future of modern development with BoxLang.

Ortus Solutions is thrilled to announce its participation as a Platinum Sponsor and Keynote Presenter at CFCamp 2025, the premier conference for modern web development! Held at the Atomis Hotel Munich Airport by Mercure in Oberding, Germany, on May 22–23, 2025, this event is a must-attend for developers and tech enthusiasts.

As the...

Cristobal Escobar
Cristobal Escobar
December 03, 2024
BoxLang Monthly Newsletter - November Recap 2024

BoxLang Monthly Newsletter - November Recap 2024

This month, we’re excited to share the latest developments, updates, and opportunities from the BoxLang ecosystem. From groundbreaking beta releases to insightful new tools and community highlights, there’s something for every modern web developer.

Dive into our November Recap to explore how BoxLang is shaping the future of web development and discover ways to get involved. Don’t miss exclusive content, upcoming events, and special offers designed to keep you ahead of the curve in the JVM ecosystem.

Maria Jose Herrera
Maria Jose Herrera
December 02, 2024
How a ColdFusion Security Audit Can Protect Your Business from Data Breaches

How a ColdFusion Security Audit Can Protect Your Business from Data Breaches

In today’s digital landscape, security threats are evolving at an alarming rate, and your business’s ColdFusion application—whether powered by Lucee or Adobe ColdFusion—may be more vulnerable than you think. A comprehensive ColdFusion security audit is essential to safeguarding sensitive data, maintaining compliance, and protecting your organization from potential cyberattacks.

Why is a ColdFusion Security Audit Critical?

Security should be a top priority for any business oper...

Cristobal Escobar
Cristobal Escobar
December 02, 2024