ColdFusion on Wheels Blog


Building search forms with tableless models in CFWheels

This blog article was originally posted on Chris’ personal blog and is republished here with his permission.

In this post, I hope to persuade you that you will rarely ever need the Tag-based form helpers (textFieldTagselectTag, etc.) in your CFWheels apps ever again.

“How?” you ask.

The answer: through the use of a wonderful feature that we affectionately call tableless models.

How you’re probably used to coding search forms in CFWheels

So let’s code up an index form using the Tag-based helpers that you’re probably accustomed to using in this situation. The view’s job is to display a list of invoice records along with a form for narrowing by start date and end date:

<cfoutput>

#startFormTag(route="invoices", method="get")#
  #textFieldTag(name="startDate", value=params.startDate)#
  #textFieldTag(name="endDate", value=params.endDate)#
  #submitTag(value="Filter Invoices")#
#endFormTag()#

<table>
  <thead>
    <tr>
      <th>Invoice</th>
      <th>Date</th>
      <th>Amount</th>
    </tr>
  </thead>
  <tbody>
    <cfloop query="invoices">
      <tr>
        <td>#h(id)#</td>
        <td>#DateFormat(createdAt)#</td>
        <td>#DollarFormat(amount)#</td>
      </tr>
    </cfloop>
  </tbody>
</table>

</cfoutput>

This is pretty common, and I wouldn’t go as far to say that it’s wrong.

Let’s code what we need in the controller to wire everything up.

component extends="Controller" {
  function index() {
    param name="params.startDate" default="";
    param name="params.endDate" default="";
    
    local.where = [];
    
    if (IsDate(params.startDate)) {
      ArrayAppend(local.where, "createdAt >= '#params.startDate#'");
    }
    
    if (IsDate(params.endDate)) {
      local.nextDay = DateAdd("d", 1, params.endDate);
      local.nextDay = DateFormat(local.nextDay, "m/d/yyyy");
      ArrayAppend(local.where, "createdAt < '#local.nextDay#'");
    }
    
    invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
  }
}

But wait! We can’t have a startDate that occurs after the endDate. We better add a check for that in the controller:

component extends="Controller" {
  function index() {
    param name="params.startDate" default="";
    param name="params.endDate" default="";
    
    local.where = [];
    
    // Let's make sure the start date and end date jive.
    if (IsDate(params.startDate) && IsDate(params.endDate) && params.startDate > params.endDate) {
      flashInsert(error="The start date must be on or before the end date.");
    }
    
    if (IsDate(params.startDate)) {
      ArrayAppend(local.where, "createdAt >= '#params.startDate#'");
    }
    
    if (IsDate(params.endDate)) {
      local.nextDay = DateAdd("d", 1, params.endDate);
      local.nextDay = DateFormat(local.nextDay, "m/d/yyyy");
      ArrayAppend(local.where, "createdAt < '#local.nextDay#'");
    }
    
    invoices = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
  }
}

That index action is getting pretty beefy at this point. And now we’re starting to validate our data in the controller, which can quickly turn into a tangled mess after we’ve added another field or two to the form.

Cleaning up the search form with tableless models

As it turns out, models in CFWheels come with a bunch of really helpful methods for validating data. And even though we’re not using this form to save data to a database, we can still use the model validations to validate our data. Hooray!

All that we need to do is create a CFC in our models folder that represents this particular form. The initializer will contain a call to table(false), which tells CFWheels to not try to connect it to a database.

In addition to table(false), we can call all of the model validation initializers that we need to validate the data.

Lastly, we need to create a method that validates data passed into the model and runs the query if all is well.

Here is the finished product in models/InvoiceSearchForm.cfc:

component extends="Model" {
  function config() {
    // Make it tableless
    table(false);
    
    // Validations
    validatesFormatOf(properties="startDate,endDate", type="date", allowBlank=true);
    validate("startDateBeforeEndDateValidation");
  }
  
  boolean function run() {
    // Run validations and abort if failed.
    if (!this.valid()) {
      this.results = QueryNew("");
      return false;
    }
    
    // Continue with query if validation passed.
    local.where = [];
    
    if (IsDate(this.startDate)) {
      ArrayAppend(local.where, "createdAt >= '#this.startDate#'");
    }
    
    if (IsDate(this.endDate)) {
      local.nextDay = DateAdd("d", 1, this.endDate);
      local.nextDay = DateFormat(local.nextDay, "m/d/yyyy");
      ArrayAppend(local.where, "createdAt < '#local.nextDay#'");
    }
    
    this.results = model("invoice").findAll(where=ArrayToList(local.where, " AND "));
    return true;
  }
  
  private function startDateBeforeEndDateValidation() {
    if (IsDate(this.startDate) && IsDate(this.endDate) && this.startDate > this.endDate) {
      this.addError("startDate", "Start Date must be on or before End Date");
    }
  }
}

Notice that startDate and endDate become properties on the model in the this scope. This allows us to validate those properties and refer to them in an object-oriented manner.

When the run method is called, there will be a results property set on the object containing the search query.

Next, we rewire the controller to this much simpler form:

component extends="Controller" {
  function index() {
    // Note that moving this into an object named `search` will change the
    // `params` struct slightly.
    param name="params.search.startDate" default="";
    param name="params.search.endDate" default="";
    
    // We pass the `params.search` struct in as properties on the search form
    // object.
    search = model("invoiceSearchForm").new(argumentCollection=params.search);
    
    // This runs the search and adds an error message if validation fails.
    if (!search.run()) {
      flashInsert(error="There was an error with your search filters");
    }
  }
}

Much cleaner, huh? Excluding whitespace and comments, this reduces the contents of the index action from 16 lines of actual code to 5.

This methodology also improves the view because we can now use textField instead of textFieldTag, and we can display validation errors near the affected form fields:

<cfoutput>

#startFormTag(route="invoices", method="get")#
  #textField(objectName="search", property="startDate")#
  #errorMessageOn(objectName="search", property="startDate")#
  
  #textField(objectName="search", property="endDate")#
  #errorMessageOn(objectName="search", property="endDate")#
  
  #submitTag(value="Filter Invoices")#
#endFormTag()#

<table>
  <thead>
    <tr>
      <th>Invoice</th>
      <th>Date</th>
      <th>Amount</th>
    </tr>
  </thead>
  <tbody>
    <cfloop query="search.results">
      <tr>
        <td>#h(id)#</td>
        <td>#DateFormat(createdAt)#</td>
        <td>#DollarFormat(amount)#</td>
      </tr>
    </cfloop>
  </tbody>
</table>

</cfoutput>

As a bonus, our InvoiceSearchForm model allows us to do things like set the labels/error message labels on the form fields using property, and allows us to do most of what models allow us to do: namely validations and callbacks.

component extends="Model" {
  function config() {
    table(false);
    
    // Set property labels for form fields and related error messages.
    property(name="startDate", label="Start");
    property(name="endDate", label="End");
    
    //...
  }

  //...
}

I find this to be a nice pattern because it ties the form to the model in a fairly clean, object-oriented way: the model represents the form, so it makes sense for it to define how labels on the form should appear.

Other uses for tableless models

Here are some other ideas where tableless models are a Good Idea™:

Authentication forms
The model takes care of all authentication logic.

Password change/reset forms
Move interface-based concepts like validatesConfirmationOf out of the table-based user model and into a tableless model.

Database transactions involving multiple models
Nested properties have their limits and logic related to them can really pollute your table-based models. Handle all of the logic in a model that’s intimately involved with the form.

Reports
Have you ever found yourself in a situation where you needed to run a query involving multiple database tables, but it was unclear which model to write the query in? Tableless models are a perfect way to avoid making a random decision.

NoSQL and API integration
Are you saving your data somewhere other than a relational database? You can still model the business logic using CFWheels models.

Props

Most of this inspiration came from a similar concept known as Form Objects in Ruby on Rails. (There is a great Railscast about Form Objectshere too.)

Also, major kudos to Tony Petruzzi for adding this awesome feature into CFWheels, which made its way into the v1.3 release.


CFWheels v2.5.0 Released

This is a major milestone release of CFWheels v2.5.0 that has been in the works for over a year. As you can see nearly 34 PRs have been merged into the codebase which include many enhancements and bug fixes. In addition many changes have been made to the tooling used in the project.

Here are some of the highlights:

  • We have begun to publish SNAPSHOTS to ForgeBox.io on each successful commit to the develop branch.
  • The GitHub Actions CI scripts use the same configuration files as the local Docker testing suite. If you are inclined to contribute to the CFWheels project you will most likely want to be able to run the test suite locally in Docker containers to test your changes before you submit a PR. To run the local test suite simply type docker compose up in the root of your project. The source code is injected into the containers dynamically so it makes it easier to make changes and see them appear in the docker containers without rebuilding the containers. Look for more details on this to come in the future.
  • Every commit is now tested across a matrix of 20 combinations of CF Engines and Databases. The matrix includes CF Engines (Lucee 5, Lucee 6, Adobe ColdFusion 2016, Adobe ColdFusion 2018, Adobe ColdFusion 2021, and Adobe ColdFusion 2023) and databases (H2, MS SQL Server, PostgreSQL, and MySQL).
  • Each successful commit automatically builds two packages on ForgeBox. One for the default template and one for the core CFWheels folder.

Upgrading an Existing Project

The changes in this version are confined to the wheels directory so simply swapping out your wheels directory should be all you need to do to upgrade.

Changelog

Model Enhancements

  • PR-1183-Allow datasource argument in finders #1183 – [Adam Chapman]
  • PR-1201-Issue ORM create() fails object validation for not null columns with defaults #929 validate not nullable columns with default #1201 – [Adam Chapman]
  • PR-1202-Remove old oracle test workaround #1202 – [Adam Chapman]
  • PR-1205-issue-1182-adds-simplelock-to-sql-caching #1205 – [Adam Chapman]
  • PR-1222-Findall() performance bottleneck #1222 – [Adam Chapman]
  • PR-1223-refactor-queryCallback-with-inbuilt-query-functions #1223 – [Adam Chapman]
  • PR-1226-Invalid column not throwing exception in select argument #1226 – [Zain Ul Abideen]
  • PR-1265-improve-performance-refactor-out-listfind #1265 – [Adam Chapman]
  • PR-1260-Adds support for native query returnType #1260 – [Adam Chapman]
  • PR-1249-Removed the original IF/ELSE condition that invalidates calculated props and added condition #1240 – [Zain Ul Abideen]

View Enhancements

  • PR-1254-issue 908 enable paginationLinks() to set active class on parent #1254 – [Zain Ul Abideen]

Bug Fixes

  • PR-1227-Return a numeric value if the primary key is Numeric #1227 – [Zain Ul Abideen]
  • PR-1257-Checkbox bug when checkedvalue is not true #1257 – [Adam Chapman]
  • PR-1246-set the default route if it is not passed in the function #1246 – [Zain Ul Abideen]
  • PR-1256-issue 889 unable to duplicate component #1256 – [Zain Ul Abideen]
  • PR-1253-Issue 580 select ambiguous column name using the wheels alias #1253 – [Zain Ul Abideen]
  • PR-1245-Added afterFind callback hook in the findAll function in case of structs #1245 – [Zain Ul Abideen]
  • PR-1302-Check for Reload Password when setting a url IP exception #1302 – Peter Amiri

Miscellaneous

  • PR-1175-restoreTestRunnerApplicationScope setting #1175 – [Adam Chapman]
  • PR-1176-fix text in core readme file #1176 – [Per Djurner]
  • PR-1177-fix text in base template readme file #1177 – [Per Djurner]
  • PR-1178-fix text in default template file #1178 – [Per Djurner]
  • PR-1185-adds-root-docker-volume #1185 – [Adam Chapman]
  • PR-1200-Update the docker-compose command to docker compose v2 syntax #1200 – [Adam Chapman, Peter Amiri]
  • PR-1204-Add Lucee 6 to test matrix on local Docker test suite #1204 – [Peter Amiri]
  • PR-1203-ensure testing params maintained #1203 – [Adam Chapman]
  • PR-1228-Adding addClass attribute in the function textField #1228 – [Zain Ul Abideen]
  • PR-1230-Add Adobe 2021 Support to local Docker and GitHub Actions testing – #1230 – Peter Amiri
  • PR-1264-update Lucee 6 version used for tests to latest #1264 – [Zac Spitzer – * New Contributor *]
  • PR-1241-Fix spelling and remove whitespace from link #1241 – [John Bampton]
  • PR-1247-show the current git branch in the debug layout #1247 – [Michael Diederich]
  • PR-1250-Added test framework functions in the docs #1250 – [Zain Ul Abideen]
  • PR-1255-issue 1179 Downloaded the CDN files and changed paths in files #1255 – [Zain Ul Abideen]

Guides

  • PR-1198-Documentation-fixes #1198 – [Adam Chapman]

Download Zip File


CFWheels on CF Alive the Sequel

Back in February of 2019 David Belanger and Tom King from the CFWheels core team sat down with Michaela Light on the ColdFusion Alive Podcast to have a chat about CFWheels. A few weeks ago Peter Amiri had a chance to speak to Michaela Light about recent developments in the CFWheels community, how to contribute to the project, and the road map ahead.

Episode 122 of the ColdFusion Alive Podcast

You can view the episode notes on the TeraTech website.


The CFWheels Channel on CFML Slack Has Been Archived

Back in May 2022 we posted a blog article announcing that CFWheels has moved to GitHub Discussions. At the time this effected the retirement of the Google mailing list and redirecting the links on the home page to the new GitHub Discussions site. Now the time has come to also retire the CFWheels channel on the CFML Slack instance.

Feel free to go back and read the original post from May that laid out the reasoning for this move but we thought it was important to reiterate our thought process once again. At the core, the reasons for this move are to move our discussions closer to the code, allowing the poster and respondent to more easily link to specific branches, files, and even lines of code. Issues can be converted to discussions if they warrant further community input or discussions promoted to an issue once an issue or feature has had open consultation and next steps identified. Discussions can be marked as answered and the specific answer identified for future reference. And most importantly, all these discussions, collaborations, and consultations are searchable and discoverable by search engines so the community as a whole reaps the benefits.

We have seen how the move to GitHub discussions by other open source communities has benefited their community and brought their discussions closer to the codebase. We hope this move by us will be similarly fruitful.


CFWheels v2.4.0 Released

This version is the accumulation of bug fixes and minor enhancements over the last quarter. This release welcomes John Bampton and Coleman Sperando, two first time contributors to the project.

Download Zip

If updating from CFWheels 2.3.x:

It should be an easy upgrade, just swap out the wheels folder.

If you installed CFWheels with CommandBox and have a box.sjon file:

Enter install cfwheels in the root of your site to update your wheels folder to the latest.

Changelog

Bug Fixes

  • issue-1091-wheels-paths-in-error-template #1091 – [Adam Chapman]
  • issue-1082-validations should not trim properties #1082 – [Adam Chapman]
  • issue-1088-Adds SQL parsing regex tweak which correctly handles whitespace #1088 – [Adam Chapman, Adam Cameron]

Miscellaneous

  • Adds cfformat ignore marker comments around core “view” cfm files that contain html markup – [Adam Chapman]
  • Adds the ability to scroll large items horizontally in the test runner UI #1130 – [Adam Chapman]
  • Fix cfformat ignore markers #1129 – [Adam Chapman]
  • Enable finder model methods to returnAs “sql”, mainly for debugging #1141 – [Adam Chapman]
  • Show the Test Runner buttons in the CFWheels GUI on the Package List screen allowing the developer to run the entire test suite instead of one package at a time. – [Peter Amiri]
  • The Base Template now contains all necessary placeholders for the CLI to interact with the application and be able to inject code properly. – [Peter Amiri]
  • By default the Core tests will run in the application datasource, but the developer can setup a different database for running the Core tests to ensure there is no side effects from running the tests. If you do end up setting a different database for the coreTestDatasourceName, make sure to reload your application after running the Core tests. – [Peter Amiri]
  • Fix two broken links in README. [#1150] – [John Bampton – * New Contributor *]
  • Fix spelling [#1151][#1158] – [John Bampton – * New Contributor *]
  • Add .env parser to parse .env files and add the properties found in the file to this.env scope. #1157 – [Peter Amiri]
  • Update the local test suite to supported ARM architecture docker images to make the suite compatible with the Apple Silicon Macs. #1143 – [Peter Amiri]

Guides

  • Fix broken links throughout the guides. – [Peter Amiri]
  • Fixed mailto link in CONTRIBUTING.md #1123 – [Coleman Sperando * New Contributor *]
  • Fix test guides examples #1125 [Adam Chapman]
  • Fix typos in the guides #1161 [Adam Chapman]

CFWheels HTMX Plugin published

A few weeks ago I published a Todo app using CFWheels on the backend and HTMX to provide the interactivity on the front end to make the app look and feel like a full blown SPA app. As I was developing that app I ran into a few things that I wish we had to make development with HTMX a little easier. But I’m getting ahead of myself.

What is HTMX

Well, HTMX was released a couple of years ago and in that short time has just about exploded in the django community. So what is HTMX, HTMX tries to answer the following questions:

  • Why should only <a> and <form> be able to make HTTP requests?
  • Why should only click & submit events trigger them?
  • Why should only GET & POST methods be available?
  • Why should you only be able to replace the entire screen?

By removing these arbitrary constraints, htmx completes HTML as a hypertext medium. You may even start wondering why these features weren’t in HTML in the first place. So let’s look at an examples.

  <script src="https://unpkg.com/[email protected]"></script>
  <!-- have a button POST a click via AJAX -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>

This block of code tells the browser:

When a user clicks on this button, issue an AJAX request to /clicked, and replace the entire button with the HTML response.

Let’s look at a typical anchor tag:

<a href="/blog">Blog</a>

This anchor tag tells the browser:

When a user clicks on this link, issue an HTTP GET request to ‘/blog’ and load the response content into the browser window.

You can see how HTMX feels like a familiar extension to HTML. With this in mind lets look at a following block of HTML:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML"
>
    Click Me!
</button>

This tells HTMX:

When a user clicks on this button, issue an HTTP POST request to ‘/clicked’ and use the content from the response to replace the element with the id parent-div in the DOM

So by using hx-get, hx-post, hx-put, hx-patch, or hx-delete we gain access to all the HTTP verbs. Imagine a delete button on a table row that actually issues a HTTP Delete to your backend.

The hx-trigger attribute gives us access to all the page events. HTML elements have sensible defaults, the button tag will get triggered by a click by default and an input tag will get triggered by a change event by default. But there are some special events as well, like the load event that will trigger the action when the page is initially loaded or the revealed event that will trigger the action, when the element scrolls into view. Think of an infinite scroll UX pattern where an element scrolls into view, which triggers a call to the backend to load more data that gets added to the bottom of the page.

The hx-target attribute lets you specify a different tag to target than the element that triggered the event. You have the typical CSS selectors but also some special syntax like closest TR to target the closest table row.

The last attribute shown in the example above is the hx-swap which specifies how to swap the response into the element. By default, the response replaces the innerHTML of the target element but you can just as easily replace the entire target element by using outerHTML. There are a few more designators that allow you to finely control placing the response before or after the target element in its parent element or at the begging of or end of a target’s child elements.

This is just scratching the surface of what HTMX can do but you should be getting the picture. By sprinkling in a handful of HTML attributes into your markup you can gain interactivity that was the domain of full blown JavaScript frontend frameworks in the past.

Why should we care as CFWheels developers

By default HTMX is backend agnostic. It just deals with HTML and doesn’t care what backend technology you use to generate it. This could just as easily be used in a plain vanilla CFML app or your framework of choice, hopefully it would be CFWheels since you are here reading this. Wheels has some built in features that make working with HTMX a breeze. We already have a templating system, we already have a router and controllers to intercept the HTTP request. We have a number of rendering methods that make responding to requests simple.

If the request is a for a full page, use the renderView() method or simply let the controller hand the request off to the view which in turn renders the view page. If the request is for a portion of the final page then use the renderPartial() method and return a snippet of code tucked away in a partial. The same partial could be used by your initial view page, keeping your code DRY. Sometimes, you just want to return a small bit of text or no text at all and it doesn’t make sense to build out a view or partial for every instance of these scenarios, that’s when the renderText() method comes in handy. Imagine a typical index page from a CRUD application that lists a bunch of rows of data and some action buttons on each row. Let’s assume, one of these buttons is a delete button. Look at the following code:

// image this button on a table row
<button hx-delete="/products/15" 
        hx-target="closest hr"
        hx-swap="outerHTML"
        hx-confirm="Are you sure?">
    Delete
</button>

// imagine this code in your action
function delete() {
  aProduct = model("product").findByKey(params.key);
  aProduct.delete();
  renderText("");
}

So what does the above combination do:

When the user clicks on the Delete button, prompt the user to make sure they are sure they wish to delete the record, if the user affirms the request, issue a DELETE request to the server. The server in turn deletes the record and sends back an empty text response to the client. When the response comes back to the frontend, find the closes table row, and remove it from the table.

We just made an Ajax call to the server, removed the record from the database, and correspondingly updated the UI by just removing a single element from the DOM.

What does this plugin do?

By default, HTMX adds some request headers to the call sent to the backend which can be interrogated to see if the request is in fact an HTMX request. If the request is actually an HTMX request, some additional request headers are made available which can add more color to the call being processed. This plugin automatically adds these header elements to the params structure which makes them automatically available to your controller actions. This makes it easier to work with this data and incorporate it into your request processing logic. Take a look at the following example:

function index() {
  if (params.htmx.reqeust) {
    renderPartial(partial="myPartial", layout="false");
  }
}

This code block says:

When a request comes in to the index action of the controller, check to see if this is an HTMX request and if it is, respond with the mypartial partial and don’t wrap it with the layout. Otherwise respond with the index view page of the current controller.

Think of a paginated index page, where the first call to the index action sends the view with the first page of data and a button or element on the page triggers additional calls to the same action but this time only the next page of data is sent to the front end.

Installing the Plugin

To install this plugin, issue the following command from the root of your application in a CommandBox prompt:

install cfwheels-htmx-plugin

Once installed, reload your application and you’re off to the races.


Wheels CLI matures to Version 1.0

It’s hard to believe it took so long to get here but modern CFML development has come a long way thanks to tools like CommandBox and ForgeBox. The Wheels CLI is built as a CommandBox module and wouldn’t have even been possible without the support of the fine folks at Ortus Solutions.

The first commit to the repo for this project was committed back in July of 2016. It’s taken a while, that’s an understatement, to get here but Wheels itself jumped to 2.0, CommandBox matured, and we were able to put the plumbing in place to support the communication between the CLI and the running server. With nearly 300 commits in the repo, 25 commands in the CLI, and over 20 pages of documentation, it’s now time to take the alpha/beta label off send this baby out into the world.

Some of the more notable commands are wheels new to use our wizard to start a brand new project. With this command and the corresponding wheels generate app command, you can start a new Wheels project in a directory, specify the template to use, pick the CF engine to use, configure the datasource, and setup your reload password. In fact there’s a whole host of generate commands for every type of object you may want to create. There are a bunch of dbmigrate commands to interact with database migrations.

To install the CLI issue the following command:

box install cfwheels-cli

Don’t forget to check out the full CLI Commands section in the guides too.


CFWheels Added to the htmx Server-Side Examples Page

Back in March we published an example app where we took the TodoMVC spec and built a reference implementation of the app with CFWheels and htmx. Here is a quick graphic of the UI of the app.

Todo app implemented with CFWheels and htmx

This app has now been added to the list of Server-Side integration examples on the htmx website.

htmx at it’s core is html over the wire. Instead of returning JSON from backend APIs and consuming them with Javascript to build page interactivity, htmx takes a different approach. It expects actual html snippets to be returned that are swapped into the DOM. How it does it, is by extending html with a handful of additional attributes, that probably should have been there in the first place, which enable any HTML element to issue an AJAX call to the backend, specify what triggers the call, and specify how the returned HTML should be added to the DOM.

We’ll be doing more with HTMX in the future so it’s great to get CFWheels on the radars of the htmx project.


Two New Repositories Published

We have published two new repositories named cfwheels/cfwheels-www and cfwheels/cfwheels-api which handle the CFWheels.org landing page site and the api.cfwheels.org API documentation site respectively.

The cfwheels-api repository is a good example of how you can structure your own projects and keep sensitive information and the core framework files out of your project’s source control repository. The project used a .gitignore file that keeps these files out of the repo. It also uses a box.json file to specify some dependencies to pull the required files back in.

Using dependencies in a box.json file is nothing new but what’s interesting is that cfwheels/cfwheels-api has three dependencies defined. One is the core framework itself and pulls in the latest framework folder. The second is the newly published CFWheels DotEnvSettings Plugin which allows keeping sensitive information out of the repository by using a locally defined .env file. The last dependency is probably the most interesting, cause that one pulls in the Semantic Version module from ForgeBox which is a module that wasn’t specifically written for CFWheels but can be pulled in and used none the less. This opens up the potential of using many more modules from ForgeBox instead of being confined to just CFWheels Plugins.


CFWheels DotEnvSettings Plugin published

A new plugin was published to provide support for .env settings files in a production environment. This plugin is based on Eric Peterson’s CommandBox module and allows the use of .env or similarly named files to store your application secrets so they can be kept out of source control.

LICENSE

Apache License, Version 2.0.

SYSTEM REQUIREMENTS

  • Lucee 5+
  • ColdFusion 9+
  • CFWheels 2+

Instructions

Just drop the zip file into your plugins folder and restart your application or use CommandBox cli to install. Simply type the following at the root of your project:

box install cfwheels-dotenvsettings

Usage

Create a .env file in your project root and add to .gitignore or your version control’s equivalent (don’t commit secrets to your repo!) The file can contain JSON or Java properties style key value pairs:

// property style
MY_SECRET_KEY=somevalue
MY_OTHER_SECRET=shh

// json style
{
  "MY_SECRET_KEY"="somevalue"
 ,"MY_OTHER_SECRET"="shh"
};

The default file name supported is .env but you can use any file name you want. You could even have multiple files for various environment.

In your CFWheels app you can read in your secrets by adding readDotEnvSettings() to your application. The most logical point to do this would be in your events\onApplicationStart.cfm file, but you could do it in your environment specific settings.cfm files as well. You can then access the secrets contained in the file processed using get("MY_SECRET_KEY").

If you want to customize the secrets file to use, you can specify the file name by passing in the file name like so readDotEnvSettings(".env-second").