Making Modules

Utilizing Reusable Code through ColdBox Modules

Follow Along:

What This Talk Is

  • An overview of modules
  • Module + ColdBox Superpowers
  • Testing strategies for modules
  • Ways to quickly go from idea to ForgeBox

Who Am I?

Utah

Ortus Solutions

Prolific Module Author

1 wife, 3 kids

Type 1 Diabetic

What is a Module?

Reusable packages of funcationality

/**
 * Returns the character at a certain position in a string.
 * 
 * @param str      String to be checked. 
 * @param pos      Position to get character from. 
 * @return Returns a character. 
 * @author Raymond Camden (ray@camdenfamily.com) 
 * @version 1, December 3, 2001 
 */
function CharAt(str,pos) {
    return Mid(str,pos,1);
}

So what defines a module?

Reusable

Semantically versioned

Can depend on other modules

Shared on forgebox

Why Use Modules?

Encapsulate complexity

Encapsulate complexity

Figure out the java interop once

Easier to test in isolation

Enforce coding standards

Build on the shoulders of giants

Do I need to be using coldbox to use modules?

No...

Benefits of modules without coldbox

  • Versioning
  • Dependency Management
  • Installation Location by Convention (or Setting)

What do I need to make a module?

box.json

Describes your module

  • Version
  • Dependencies
  • Module Type
  • Installation Data
  • Package Scripts
  • Other Metadata
box.json
box.json
{
    "name":"cbyaml",
    "version":"1.0.1",
    "author":"Eric Peterson",
    "location":"elpete/cbyaml#v1.0.1",
    "homepage":"https://github.com/elpete/cbyaml",
    "documentation":"https://github.com/elpete/cbyaml",
    "repository":{
        "type":"git",
        "URL":"https://github.com/elpete/cbyaml"
    },
    "bugs":"https://github.com/elpete/cbyaml/issues",
    "slug":"cbyaml",
    "shortDescription":"Provides easy serialization and deserialization of yaml files",
    "description":"Provides easy serialization and deserialization of yaml files",
    "type":"modules",
    "dependencies":{
        "cbjavaloader":"^2.0.0+44"
    },
    "devDependencies":{
        "coldbox":"^5.6.2+1021",
        "testbox":"^3.0.0"
    },
    "installPaths":{
        "testbox":"testbox/",
        "coldbox":"tests/resources/app/coldbox/",
        "cbjavaloader":"modules/cbjavaloader/"
    },
    "scripts":{
        "postVersion":"package set location='elpete/cbyaml#v`package version`'"
    },
    "ignore":[
        "**/.*",
        "test",
        "tests"
    ]
}

Do I need to be using coldbox to use modules?

No...

BUT...

Coldbox gives you superpowers

Not using ColdBox yet?  Check out this guide to starting one piece at a time:

https://www.ortussolutions.com/blog/coldbox-legacy-app-demo

Title Text

Subtitle

What do I get with ColdBox modules?

Self-contained ColdBox Applications

What does that mean?

  • Full MVC Sub-application
  • Models
  • Interceptors
  • Layouts & Views
  • Handlers
  • Overridable Settings
  • Automatic WireBox Registration

So how do we take advantage of all this?

ModuleConfig.cfc

Bootstraps your module

  • Version
  • Dependencies
  • Module Type
  • Installation Data
  • Package Scripts
  • Other Metadata
ModuleConfig.cfc

Properties

ModuleConfig.cfc
Property Purpose
name The unique name of the module
entryPoint The default route into this module (i.e. /api/v1)
cfmapping The mapping to access this module
dependencies Other modules that must be loaded before this module
autoMapModels Set to true to have WireBox automatically map your models folder

Methods

ModuleConfig.cfc
Method Purpose
configure() Only interact with this module. Use onLoad() for cross-module, cross-framework dependencies.
onLoad() When you need the framework loaded first.
onUnload() Undo what you did in load.

WireBox Integration

ModuleConfig.cfc
this.autoMapModels = true;

/*
Automatically maps all of your models to
{modelName}@{moduleName}

property name="builder" inject="Builder@qb";
*/

Advanced WireBox Integration

ModuleConfig.cfc
function configure() {
    binder.map( "DefaultGrammar" )
        .to( "#moduleMapping#.models.grammars.MySQLGrammar" );
}

BONUS:

Moduleconfig.cfc

is also an Interceptor

component {

    this.name = "JSONToRC";
    this.author = "Eric Peterson";
    this.description = "Add the HTTP Request Body to the RC on each request";
    this.version = "1.0.0";

    function configure() {}

    function preProcess( event, interceptData, buffer, rc, prc ) {
        var jsonContent = event.getHTTPContent( json = true );

        if ( isStruct( jsonContent ) ) {
            structAppend( rc, jsonContent );
        }
    }

}

Examples Of Modules

Single Purpose

  • vue-helpers
  • redirectBack
  • JSONToRC

Utility Libraries

  • str
  • bcrypt
  • cbvalidation
  • mementifier

API Wrappers

  • cbgithub
  • sendgrid-sdk
  • aws-cfml

Interceptors

  • verify-csrf-interceptor
  • cors
  • ses-on-request

Frameworks

  • qb
  • Quick
  • hyper

Module Deep Dive

redirectBack
redirectBack

Usage

component {

    property name="auth" inject="AuthenticationService@cbauth";
    property name="flash" inject="coldbox:flash";

    function new( event, rc, prc ){
        param prc.errors = flash.get( "registration_form_errors", {} );
        event.setView( "registrations/new" );
    }

    function create( event, rc, prc ){
        var result = validateModel(
            target = rc,
            constraints = {
                "email" : {
                    "required" : true,
                    "type" : "email",
                    "uniqueInDatabase" : { "table" : "users", "column" : "email" }
                },
                "password" : { "required" : true },
                "passwordConfirmation" : { "required" : true, "sameAs" : "password" }
            }
        );

       if ( result.hasErrors() ) {
            flash.put( "registration_form_errors", result.getAllErrorsAsStruct() );
            redirectBack();
            return;
        }

        var user = getInstance( "User" ).create( { "email" : rc.email, "password" : rc.password } );

        auth.login( user );

        relocate( uri = "/" );
	}

}
redirectBack

Directory Structure

redirectBack
box.json
{
    "name": "redirectBack",
    "version": "1.0.4",
    "location": "elpete/redirectBack#v1.0.4",
    "slug": "redirectBack",
    "shortDescription":
        "Caches the last request in the flash scope to give easy redirects back",
    "type": "modules",
    "scripts": {
        "postVersion": "package set location='elpete/redirectBack#v`package version`'",
        "onRelease": "publish",
        "postPublish": "!git push && git push --tags"
    },
    "ignore": [
        "**/.*",
        "test",
        "tests"
    ]
}
redirectBack
ModuleConfig.cfc
this.title = "redirectBack";
this.author = "Eric Peterson";
this.webURL = "https://github.com/elpete/redirectBack";
this.description = "Caches the last request in the flash scope to give easy redirects back";
this.version = "1.0.4";

Properties

redirectBack
ModuleConfig.cfc
function configure() {
    /*
     * can override any of these settings in your `config/ColdBox.cfc` file.
     * `moduleSettings = { redirectBack = { key = "overridden_key" } };`
     * Or in CommandBox * `config set modules.redirectBack.key="overridden_key"`
     */
    settings = {
        "key" = "last_url"
    };
  
    interceptors = [
        {
            class = "#moduleMapping#/interceptors/RedirectBack",
            name = "RedirectBack",
            properties = {}
        }
    ];
}
configure()
redirectBack
ModuleConfig.cfc
/*
 * We use onLoad instead of configure here
 * because we want to interact with other
 * ColdBox settings, such as application helpers.
 */
function onLoad() {
    var helpers = controller.getSetting( "applicationHelper" );
    arrayAppend( helpers, "#moduleMapping#/helpers/RedirectBackHelpers.cfm" );
    controller.setSetting( "applicationHelper", helpers );
}
onLoad()
redirectBack
ModuleConfig.cfc
function onUnload() { 
    controller.setSetting(
        "applicationHelper",
        arrayFilter( controller.getSetting( "applicationHelper" ), function( helper ) {
            return helper != "#moduleMapping#/helpers/RedirectBackHelpers.cfm";
        } )
    );
}
onUnload()
redirectBack
interceptors/RedirectBack.cfc
component extends="coldbox.system.Interceptor" {
  
    property name="moduleSettings" inject="coldbox:moduleSettings:redirectBack";

    function postProcess( event, interceptData, buffer, rc, prc ) {
        var flash = wirebox.getInstance( dsl = "coldbox:flash" );

        if ( ! event.isAjax() ) {
            flash.put(
                name = moduleSettings.key,
                value = event.isSES() ? event.getCurrentRoutedUrl() : event.getCurrentEvent(),
                autoPurge = false
            );
        }

        
    }

}
redirectBack
RedirectBackHelpers.cfm
function redirectBack() {
    var moduleSettings = wirebox.getInstance( dsl = "coldbox:moduleSettings:redirectBack" );
    var flash = wirebox.getInstance( dsl = "coldbox:flash" );
    arguments.event = flash.get( moduleSettings.key, "" );
    relocate( argumentCollection = arguments );
}

Deep Dive #2

cbyaml

To the Code!!

Other notable Modules

To the Code!!

Where Do I find Modules?

Git Repo

modules_app

Other Module Locations

Pros

Cons

  • Versioning (via tags)
  • Can be shared (Public repos)
  • Can be private (Private Repos)

  • No semantic version ranges
  • No ForgeBox slugs

Pros

Cons

  • Committed in your repo
  • Private to your application
  • Easy to iterate on and develop

  • Great for internal API versions

  • No reuse outside your project
  • No versions

You said modules are easier to test?

Unit Testing

component extends="testbox.system.BaseSpec" {
    
    function beforeAll() {
        include "/root/functions/normalizeToArray.cfm";
    }
    
    function run() {
        describe( "normalizeToArray", function() {
            it( "returns an array unmodified", function() {
                var actual = normalizeToArray( [ 1, 2, 3, 4 ] );
                expect( actual ).toBe( [ 1, 2, 3, 4 ] );
            } );
            
            it( "converts a list to an array", function() {
                var actual = normalizeToArray( "1,2,3,4" );
                expect( actual ).toBe( [ 1, 2, 3, 4 ] );
            } );
        } );
    }

}

Integration Testing

component extends="tests.resources.ModuleIntegrationSpec" appMapping="/app" {

    variables.travisYmlSamplePath = expandPath(
        "/tests/resources/sample-files/.travis.sample.yml"
    );

    function run() {
        describe( "Yaml Parser", function() {
            it( "can deserialize yaml strings", function() {
                var parser = getWireBox().getInstance( "Parser@cbyaml" );
                var travisYmlSample = fileRead( variables.travisYmlSamplePath );
                var actual = parser.deserialize( travisYmlSample );
                expect( actual ).toBe( getTravisYmlAsCF() );
            } );
        } );
    }

    function getTravisYmlAsCF() { /* ... */ }

}

Scaffolding Modules

Steps to share a module

  • Create box.json
  • Create ModuleConfig.cfc
  • Scaffold tests folder
  • Copy over Travis CI configuration
  • Create git repo
  • Create GitHub repo

For ColdBox Applications

coldbox create app

For New Modules

install cb-module-template
module scaffold my-awesome-module "It will blow your mind!"

Recap

  • Build reusable code
  • Test easier
  • Share with others

Thanks!

@_elpete

elpete

dev.elpete.com

Making Modules

By Eric Peterson

Making Modules

Utilizing reusable code through ColdBox Modules

  • 2,448