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"
]
}
Discover the full schema at https://commandbox.ortusbooks.com/package-management/box.json
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:
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 |
Find all the properties at
https://coldbox.ortusbooks.com/hmvc/modules/moduleconfig/public-module-properties-directives
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
Copy of Making Modules
By Eric Peterson
Copy of Making Modules
Utilizing reusable code through ColdBox Modules
- 534