Only this pageAll pages
Powered by GitBook
1 of 84

3.0.0-SNAPSHOT

INTRODUCTION

Loading...

Loading...

Loading...

Loading...

Loading...

Frameworks and Wheels

Loading...

Loading...

Loading...

Loading...

Command Line Tools

Loading...

Loading...

Loading...

Loading...

Loading...

Working with Wheels

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Handling Requests with Controllers

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Displaying Views to Users

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Database Interaction Through Models

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Plugins

Loading...

Loading...

Loading...

Project Documentation

Loading...

External Links

Requirements

What you need to know and have installed before you start programming in Wheels.

We can identify 3 different types of requirements that you should be aware of:

  • Project Requirements. Is Wheels a good fit for your project?

  • Developer Requirements. Do you have the knowledge and mindset to program effectively in Wheels?

  • System Requirements. Is your server ready for Wheels?

Project Requirements

Before you start learning Wheels and making sure all the necessary software is installed on your computer, you really need to take a moment and think about the project you intend to use Wheels on. Is it a ten page website that won't be updated very often? Is it a space flight simulator program for NASA? Is it something in between?

Most websites are, at their cores, simple data manipulation applications. You fetch a row, make some updates to it, stick it back in the database and so on. This is the "target market" for Wheels--simple CRUD (create, read, update, delete) website applications.

A simple ten page website won't do much data manipulation, so you don't need Wheels for that (or even ColdFusion in some cases). A flight simulator program will do so much more than simple CRUD work, so in that case, Wheels is a poor match for you (and so perhaps, is ColdFusion).

If your website falls somewhere in between these two extreme examples, then read on. If not, go look for another programming language and framework. ;)

Another thing worth noting right off the bat (and one that ties in with the simple CRUD reasoning above) is that Wheels takes a very data-centric approach to the development process. What we mean by that is that it should be possible to visualize and implement the database design early on in the project's life cycle. So, if you're about to embark on a project with an extensive period of object oriented analysis and design which, as a last step almost, looks at how to persist objects, then you should probably also look for another framework.

Still reading?

Good!

Moving on...

Developer Requirements

Yes, there are actually some things you should familiarize yourself with before starting to use Wheels. Don't worry though. You don't need to be an expert on any on of them. A basic understanding is good enough.

  • CFML. You should know CFML, the ColdFusion programming language. (Surprise!)

  • Object Oriented Programming. You should grasp the concept of object oriented programming and how it applies to CFML.

  • Model-View-Controller. You should know the theory behind the Model-View-Controller development pattern.

CFML

Object Oriented Programming (OOP)

This is a programming methodology that uses constructs called objects to design applications. Objects model real world entities in your application. OOP is based on several techniques including inheritance, modularity, polymorphism, and encapsulation. Most of these techniques are supported in CFML, making it a fairly functional object oriented language. At the most basic level, a .cfc file in CFML is a class, and you create an instance of a class by using the CreateObject function or the <cfobject> tag.

Trying to squeeze an explanation of object oriented programming and how it's used in CFML into a few sentences is impossible, and a detailed overview of it is outside the scope of this chapter. There is lots of high quality information online, so go ahead and Google it.

Model-View-Controller

Model-View-Controller, or MVC for short, is a way to structure your code so that it is broken down into 3 easy-to-manage pieces:

  • Model. Just another name for the representation of data, usually a database table.

  • View. What the user sees and interacts with (a web page in our case).

  • Controller. The behind-the-scenes guy that's coordinating everything.

MVC is how Wheels structures your code for you. As you start working with Wheels applications, you'll see that most of the code you write is very nicely separated into one of these 3 categories.

System Requirements

Wheels requires that you use one of these CFML engines:

Operating Systems

Your ColdFusion or Lucee engine can be installed on Windows, Mac, UNIX, or Linux—they all work just fine.

Web Servers

Database Engines

Finally, to build any kind of meaningful website application, you will likely interact with a database. These are the currently supported databases:

  • SQL Server 7+

  • MySQL 5+ *

  • PostgreSQL 8.4+

  • H2 1.4+

MySQL

  • Wheels maybe incompatible with newer MySQL JDBC drivers. It is recommended you downgrade the driver to version 5.1.x for full ORM functionality.

  • MySQL 4 is not supported.

OK, hopefully this chapter didn't scare you too much. You can move on knowing that you have the basic knowledge needed, the software to run Wheels, and a suitable project to start with.

Beginner Tutorial: Hello Database

A quick tutorial that demonstrates how quickly you can get database connectivity up and running with Wheels.

Wheels's built in model provides your application with some simple and powerful functionality for interacting with databases. To get started, you will make some simple configurations, call some functions within your controllers, and that's it. Best yet, you will rarely ever need to write SQL code to get those redundant CRUD tasks out of the way.

Our Sample Application: User Management

We'll learn by building part of a sample user management application. This tutorial will teach you the basics of setting up a resource that interacts with the Wheels ORM.

Download source code

Setting up the Data Source

By default, Wheels will connect to a data source wheels.dev. To change this default behavior, open the file at app/config/settings.cfm. In a fresh install of Wheels, you'll see the follwing code:

app/config/settings.cfm
<cfscript>
	/*
		Use this file to configure your application.
		You can also use the environment specific files (e.g. app/config/production/settings.cfm) to override settings set here.
		Don't forget to issue a reload request (e.g. reload=true) after making changes.
		See https://guides.wheels.dev/2.5.0/v/3.0.0-snapshot/working-with-wheels/configuration-and-defaults for more info.
	*/

	/*
		You can change the "wheels.dev" value from the two functions below to set your datasource.
		You can change the the value for the "dataSourceName" to set a default datasource to be used throughout your application.
		You can also change the value for the "coreTestDataSourceName" to set your testing datasource.
	*/
	set(coreTestDataSourceName="wheels.dev");
	set(dataSourceName="wheels.dev");
    // set(dataSourceUserName="");
    // set(dataSourcePassword="");

	/*
		If you comment out the following line, Wheels will try to determine the URL rewrite capabilities automatically.
		The "URLRewriting" setting can bet set to "on", "partial" or "off".
		To run with "partial" rewriting, the "cgi.path_info" variable needs to be supported by the web server.
		To run with rewriting set to "on", you need to apply the necessary rewrite rules on the web server first.
	*/
	set(URLRewriting="On");

	// Reload your application with ?reload=true&password=wheels.dev
	set(reloadPassword="wheels.dev");

	// CLI-Appends-Here
</cfscript>

These lines provide Wheels with the necessary information about the data source, URL rewriting, and reload password for your application, and include the appropriate values. This may include values for dataSourceName, dataSourceUserName, and dataSourcePassword. More on URL rewriting and reload password later.

app/config/settings.cfm
set(dataSourceName="back2thefuture");
// set(dataSourceUserName="marty");
// set(dataSourcePassword="mcfly");

Our Sample Data Structure

Wheels supports MySQL, SQL Server, PostgreSQL, and H2. It doesn't matter which DBMS you use for this tutorial; we will all be writing the same CFML code to interact with the database. Wheels does everything behind the scenes that needs to be done to work with each DBMS.

That said, here's a quick look at a table that you'll need in your database, named users:

Column Name
Data Type
Extra

id

int

auto increment

username

varchar(100)

email

varchar(255)

passwd

varchar(15)

Note a couple things about this users table:

  1. The table name is plural.

  2. The table has an auto-incrementing primary key named id.

Fortunately, there are ways of going outside of these conventions when you really need it. But let's learn the conventional way first. Sometimes you need to learn the rules before you can know how to break them.

Creating Routes for the users Resource

Next, open the file at app/config/routes.cfm. You will see contents similar to this:

app/config/routes.cfm
mapper()
    .wildcard()
    .root(method = "get")
.end();

We are going to create a section of our application for listing, creating, updating, and deleting user records. In Wheels routing, this requires a plural resource, which we'll name users.

Because a users resource is more specific than the "generic" routes provided by Wheels, we'll list it first in the chain of mapper method calls:

app/config/routes.cfm
mapper()
    .resources("users")
    .wildcard()
    .root(method = "get")
.end();

This will create URL endpoints for creating, reading, updating, and deleting user records:

Name
Method
URL Path
Description

users

GET

/users

Lists users

newUsers

GET

/users/new

Display a form for creating a user record

users

POST

/users

Form posts a new user record to be created

editUser

GET

/users/[id]/edit

Displays a form for editing a user record

user

PATCH

/users/[id]

Form posts an existing user record to be updated

user

DELETE

/users/[id]

Deletes a user record

  • Name is referenced in your code to tell Wheels where to point forms and links.

  • Method is the HTTP verb that Wheels listens for to match up the request.

  • URL Path is the URL that Wheels listens for to match up the request.

Don't forget to reload

You will need to reload your application after adding new routes!

Creating Users

First, let's create a simple form for adding a new user to the users table. To do this, we will use Wheels's form helper functions. Wheels includes a whole range of functions that simplifies all of the tasks that you need to display forms and communicate errors to the user.

Creating the Form

Now create a new file in app/views/users called new.cfm. This will contain the view code for our simple form.

Next, add these lines of code to the new file:

app/views/users/new.cfm
<cfoutput>

<h1>New User</h1>

#startFormTag(route="users")#
    <div>
        #textField(objectName="user", property="username", label="Username")#
    </div>

    <div>
        #textField(objectName="user", property="email", label="Email")#
    </div>

    <div>
        #passwordField(
            objectName="user",
            property="passwd",
            label="Password"
        )#
    </div>

    <div>#submitTag()#</div>
#endFormTag()#

</cfoutput>

Form Helpers

What we've done here is use form helpers to generate all of the form fields necessary for creating a new user in our database. It may feel a little strange using functions to generate form elements, but it will soon become clear why we're doing this. Trust us on this one… you'll love it!

Supplying the Form with Data

All of the form helper calls in our view specify an objectName argument with a reference to a variable named user. That means that we need to supply our view code with an object called user. Because the controller is responsible for providing the views with data, we'll set it there.

Create a new ColdFusion component at app/controllers/Users.cfc.

app/controllers/Users.cfc
component extends="Controller" {
    function config(){}

    function new() {
        user = model("user").new();
    }
}

Wheels will automatically know that we're talking about the users database table when we instantiate a user model. The convention: database tables are plural and their corresponding Wheels models are singular.

Why is our model name singular instead of plural? When we're talking about a single record in the users database, we represent that with an individual model object. So the users table contains many user objects. It just works better in conversation.

The Generated Form

Now when we run the URL at http://localhost/users/new, we'll see the form with the fields that we defined.

The HTML generated by your application will look something like this:

/users/new
<h1>New User</h1>

<form action="/users" method="post">
    <div>
        <label for="user-username">
            Username
            <input id="user-username" type="text" value="" name="user&#x5b;username&#x5d;">
        </label>
    </div>

    <div>
        <label for="user-email">
            Email
            <input id="user-email" type="text" value="" name="user&#x5b;email&#x5d;">
        </label>
    </div>

    <div>
        <label for="user-passwd">
            Password
            <input id="user-passwd" type="password" value="" name="user&#x5b;passwd&#x5d;">
        </label>
    </div>

    <div><input value="Save&#x20;changes" type="submit"></div>
</form>

So far we have a fairly well-formed, accessible form, without writing a bunch of repetitive markup.

Handling the Form Submission

Next, we'll code the create action in the controller to handle the form submission and save the new user to the database.

app/controllers/Users.cfc
function create() {
    user = model("user").create(params.user);

    redirectTo(
        route="users",
        success="User created successfully."
    );
}

Because we used the objectName argument in the fields of our form, we can access the user data as a struct in the params struct.

There are more things that we can do in the create action to handle validation, but let's keep it simple in this tutorial.

Listing Users

First, let's get the data that the listing needs. Create an action named index in the users controller like so:

app/controllers/Users.cfc
function index() {
    users = model("user").findAll(order="username");
}

In the view at app/views/users/index.cfm, it's as simple as looping through the query and outputting the data

app/views/users/index.cfm
<cfoutput>

<h1>Users</h1>

<p>#linkTo(text="New User", route="newUser")#</p>

<table>
    <thead>
        <tr>
            <th>Username</th>
            <th>Email</th>
            <th colspan="2"></th>
        </tr>
    </thead>
    <tbody>
        <cfloop query="users">
            <tr>
                <td>
                    #EncodeForHtml(users.username)#
                </td>
                <td>
                    #EncodeForHtml(users.email)#
                </td>
                <td>
                    #linkTo(
                        text="Edit",
                        route="editUser",
                        key=users.id,
                        title="Edit #users.username#"
                    )#
                </td>
                <td>
                    #buttonTo(
                        text="Delete",
                        route="user",
                        key=users.id,
                        method="delete",
                        title="Delete #users.username#"
                    )#
                </td>
            </tr>
        </cfloop>
    </tbody>
</table>

</cfoutput>

When to use EncodeForHtml

You'll see references to EncodeForHtml in some of our examples that output data. This helps escape HTML code in data that attackers could use to embed inject harmful JavaScript. (This is commonly referred to as an "XSS attack," short for "Cross-site Scripting attack.")

A rule of thumb: you do not need to use EncodeForHtml when passing values into Wheels helpers like linkTo, buttonTo, startFormTag, textField, etc. However, you need to escape data that is displayed directly onto the page without a Wheels helper.

Editing Users

We'll now show another cool aspect of form helpers by creating a screen for editing users.

Coding the Edit Form

You probably noticed in the code listed above that we'll have an action for editing a single users record. We used the linkTo() form helper function to add an "Edit" button to the form. This action expects a key as well.

Because in the linkTo() form helper function we specified the parameter key, Wheels adds this parameter into the URL when generating the route.

Wheels will automatically add the provided 'key' from the URL to the params struct in the controllers edit() function.

Given the provided key, we'll have the action load the appropriate user object to pass on to the view:

app/controllers/Users.cfc
function edit() {
    user = model("user").findByKey(params.key);
}

The view at app/views/users/edit.cfm looks almost exactly the same as the view for creating a user:

app/views/users/edit.cfm
<cfoutput>

<h1>Edit User #EncodeForHtml(user.username)#</h1>

#startFormTag(route="user", key=user.key(), method="patch")#
    <div>
        #textField(objectName="user", property="username", label="Username")#
    </div>

    <div>
        #textField(objectName="user", property="email", label="Email")#
    </div>

    <div>
        #passwordField(
            objectName="user",
            property="passwd",
            label="Password"
        )#
    </div>

    <div>#submitTag()#</div>
#endFormTag()#

</cfoutput>

But an interesting thing happens. Because the form fields are bound to the user object via the form helpers' objectName arguments, the form will automatically provide default values based on the object's properties.

With the user model populated, we'll end up seeing code similar to this:

app/views/users/edit.cfm
<h1>Edit User Homer Simpson</h1>

<form action="/users/1" method="post">
    <input type="hidden" name="_method" value="patch">

    <div>
        <input type="hidden" name="user&#x5b;id&#x5d;" value="15">
    </div>

    <div>
        <label for="user-username">
            Name
            <input
                id="user-username"
                type="text"
                value="Homer Simpson"
                name="user&#x5b;username&#x5d;">
        </label>
    </div>

    <div>
        <label for="user-email">
            Email
            <input
                id="user-email"
                type="text"
                value="homerj@nuclearpower.com"
                name="user&#x5b;email&#x5d;">
        </label>
    </div>

    <div>
        <label for="user-passwd">
            Password
            <input
                id="user-passwd"
                type="password"
                value="donuts.mmm"
                name="user&#x5b;passwd&#x5d;">
        </label>
    </div>

    <div><input value="Save&#x20;changes" type="submit"></div>
</form>

Pretty cool, huh?

Opportunities for Refactoring

There's a lot of repetition in the new and edit forms. You'd imagine that we could factor out most of this code into a single view file. To keep this tutorial from becoming a book, we'll just continue on knowing that this could be better.

Handing the Edit Form Submission

Now we'll create the update action. This will be similar to the create action, except it will be updating the user object:

app/controllers/Users.cfc
function update() {
    user = model("user").findByKey(params.key);
    user.update(params.user);

    redirectTo(
        route="editUser",
        key=user.id,
        success="User updated successfully."
    );
}

Deleting Users

Notice in our listing above that we have a delete action. Here's what it would look like:

app/controllers/Users.cfc
function delete() {
    user = model("user").findByKey(params.key);
    user.delete();

    redirectTo(
        route="users",
        success="User deleted successfully."
    );
}

Database Says Hello

We've shown you quite a few of the basics in getting a simple user database up and running. We hope that this has whet your appetite to see some of the power packed into the Wheels framework. There's plenty more.

Be sure to read on to some of the related chapters listed below to learn more about working with Wheels's ORM.

Tutorial: Wheels, AJAX, and You

Using Wheels to develop web applications with AJAX features is a breeze. You have several options and tools at your disposal, which we'll cover in this chapter.

Wheels was designed to be as lightweight as possible, so this keeps your options fairly open for developing AJAX features into your application.

For this tutorial, let's create the simplest example of all: a link that will render a message back to the user without refreshing the page.

A Simple Example

In this example, we'll wire up some simple JavaScript code that calls a Wheels action asynchronously. All of this will be done with basic jQuery code and built-in Wheels functionality.

First, let's make sure we've got an appropriate route setup. It might be you're still using the default wildcard() route which will create some default GET routes for the controller/action pattern, but we'll add a new route here just for practice. We are going to create a route named sayHello and direct it to the hello action of the say controller. There are two ways you could write this code a long hand method specifying the controller and action separately as well as a short hand method that combines the two into a single parameter.

The longhand way would look like:

app/config/routes.cfm
mapper()
  .get(name="sayHello", controller="say", action="hello")
.end()

The shorthand method would look like:

app/config/routes.cfm
mapper()
  .get(name="sayHello", to="say##hello")
.end()

You can decide which method you prefer. Both sets of code above are equivalent.

Then, let's create a link to a controller's action in a view file, like so:

app/views/say/hello.cfm
<cfoutput>

<!--- View code --->
<h1></h1>
<p></p>

#linkTo(text="Alert me!", route="sayHello", id="alert-button")#

</cfoutput>

That piece of code by itself will work just like you expect it to. When you click the link, you will load the hello action inside the say controller.

JavaScript
(function($) {
    // Listen to the "click" event of the "alert-button" link and make an AJAX request
    $("#alert-button").on("click", function(event) {
        $.ajax({
            type: "GET",
            // References "/say/hello?format=json"
            url: $(this).attr("href") + "?format=json",
            dataType: "json",
            success: function(response) {
                $("h1").html(response.message);
                $("p").html(response.time);
            }
        });

        event.preventDefault();
    });
})(jQuery);

With that code, we are listening to the click event of the hyperlink, which will make an asynchronous request to the hello action in the say controller. Additionally, the JavaScript call is passing a URL parameter called format set to json.

Note that the success block inserts keys from the response into the empty h1 and p blocks in the calling view. (You may have been wondering about those when you saw the first example. Mystery solved.)

app/controllers/Say.cfc
component extends="Controller" {
    function config() {
        provides("html,json");
    }

    function hello() {
        // Prepare the message for the user.
        greeting = {};
        greeting["message"] = "Hi there";
        greeting["time"] = Now();

        // Respond to all requests with `renderWith()`.
        renderWith(greeting);
    }
}

Lastly, notice the lines where we're setting greeting["message"] and greeting["time"]. Due to the case-insensitive nature of ColdFusion, we recommend setting variables to be consumed by JavaScript using bracket notation like that. If you do not use that notation (i.e., greetings.message and greetings.time instead), your JavaScript will need to reference those keys from the JSON as MESSAGE and TIME (all caps). Unless you like turning caps lock on and off, you can see how that would get annoying after some time.

Assuming you already included jQuery in your application and you followed the code examples above, you now have a simple AJAX-powered web application built on Wheels. After clicking that Alert me! link, your say controller will respond back to you the serialized message via AJAX. jQuery will parse the JSON object and populate the h1 and pwith the appropriate data.

AJAX in Wheels Explained

That is it! Hopefully now you have a clearer picture on how to create AJAX-based features for your web applications.

Beginner Tutorial: Hello World

In this tutorial, we'll be writing a simple application to make sure we have Wheels installed properly and that everything is working as it should.

Testing Your Install

Okay, so you have Wheels installed and can see the Wheels "Congratulations!" page as shown below. That wasn't that hard now, was it?

Hello World: Your First Wheels App

Okay, let's get to some example code. We know that you've been dying to get your hands on some code!

To continue with Programming Tutorial Tradition, we'll create the ubiquitous Hello World! application. But to keep things interesting, let's add a little Wheels magic along the way.

Setting up the Controller

Let's create a controller from scratch to illustrate how easy it is to set up a controller and plug it into the Wheels framework.

First, create a file called Say.cfc in the app/controllers directory and add the code below to the file.

app/controllers/Say.cfc
component extends="Controller"{
}

Congratulations, you just created your first Wheels controller! What does this controller do, you might ask? Well, to be honest, not much. It has no methods defined, so it doesn't add any new functionality to our application. But because it extends the base Controller component, it inherits quite a bit of powerful functionality and is now tied into our Wheels application.

The error says "Could not find the view page for the 'index' action in the 'say' controller." Where did "index" come from? The URL we typed in only specified a controller name but no action. When an action is not specified in the URL, Wheels assumes that we want the default action. Out of the box, the default action in Wheels is set to index. So in our example, Wheels tried to find the index action within the say controller, and it threw an error because it couldn't find its view page.

Setting up an Action

But let's jump ahead. Now that we have the controller created, let's add an action to it called hello. Change your say controller so it looks like the code block below:

app/controllers/Say.cfc
component extends="Controller" {
    function hello() {
    }
}

As you can see, we created an empty method named hello.

Now let's call our new action in the browser and see what we get. To call the hello action, we simply add /hello to the end of the previous URL that we used to call our say controller:

http://127.0.0.1:60000/say/hello

Once again, we get a ColdFusion error. Although we have created the controller and added the hello action to it, we haven't created the view.

Setting up the View

By default, when an action is called, Wheels will look for a view file with the same name as the action. It then hands off the processing to the view to display the user interface. In our case, Wheels tried to find a view file for our say/hello action and couldn't find one.

Let's remedy the situation and create a view file. View files are simple CFML pages that handle the output of our application. In most cases, views will return HTML code to the browser. By default, the view files will have the same name as our controller actions and will be grouped into a directory under the view directory. This new directory will have the same name as our controller.

Find the views directory inside the app directory, located at the root of your Wheels installation. There will be a few directories in there already. For now, we need to create a new directory in the views directory called say. This is the same name as the controller that we created above.

Now inside the say directory, create a file called hello.cfm. In the hello.cfm file, add the following line of code:

app/views/say/hello.cfm
<h1>Hello World!</h1>

Save your hello.cfm file, and let's call our say/hello action once again. You have your first working Wheels page if your browser looks like Figure 3 below.

You have just created your first functional Wheels page, albeit it is a very simple one. Pat yourself on the back, go grab a snack, and when you're ready, let's go on and extend the functionality of our Hello World! application a little more.

Adding Dynamic Content to Your View

We will add some simple dynamic content to our hello action and add a second action to the application. We'll then use some Wheels code to tie the 2 actions together. Let's get get to it!

The Dynamic Content

The first thing we are going to do is to add some dynamic content to our say/hello action. Modify your say controller so it looks like the code block below:

app/controllers/Say.cfc
component extends="Controller" {
    function hello() {
        time = Now();
    }
}

All we are doing here is creating a variable called time and setting its value to the current server time using the basic ColdFusion Now() function. When we do this, the variable becomes immediately available to our view code.

Why not just set up this value directly in the view? If you think about it, maybe the logic behind the value of time may eventually change. What if eventually we want to display its value based on the user's time zone? What if later we decide to pull it from a web service instead? Remember, the controller is supposed to coordinate all of the data and business logic, not the view.

Displaying the Dynamic Content

Next, we will modify our say/hello.cfm view file so that it looks like the code block below. When we do this, the value will be displayed in the browser.

app/views/say/hello.cfm
<h1>Hello World!</h1>
<p>Current time: <cfoutput>#time#</cfoutput></p>

call your say/hello action again in your browser. Your browser should look like Figure 4 below.

This simple example showed that any dynamic content created in a controller action is available to the corresponding view file. In our application, we created a time variable in the say/hello controller action and display that variable in our say/hello.cfm view file.

Adding a Second Action: Goodbye

Now we will expand the functionality of our application once again by adding a second action to our say controller. If you feel adventurous, go ahead and add a goodbye action to the say controller on your own, then create a goodbye.cfm view file that displays a "Goodbye" message to the user. If you're not feeling that adventurous, we'll quickly go step by step.

First, modify the the say controller file so that it looks like the code block below.

app/controllers/Say.cfc
component extends="Controller" {
    function hello() {
        time = Now();
    }

    function goodbye() {
    }
}

Now go to the app/views/say directory and create a goodbye.cfm page.

Add the following code to the goodbye.cfm page and save it.

app/views/say/goodbye.cfm
Goodbye World!

If we did everything right, we should be able to call the new say/goodbye action using the following URL:

http://127.0.0.1:60000/say/goodbye

Your browser should look like Figure 5 below:

Linking to Other Actions

Now let's link our two actions together. We will do this by adding a link to the bottom of each page so that it calls the other page.

Linking Hello to Goodbye

Open the say/hello.cfm view file. We are going to add a line of code to the end of this file so our say/hello.cfm view file looks like the code block below:

app/views/say/hello.cfm
<h1>Hello World!</h1>
<p>Current time: <cfoutput>#time#</cfoutput></p>
<p>Time to say <cfoutput>#linkTo(text="goodbye", action="goodbye")#?</cfoutput></p>

Once you have added the additional line of code to the end of the say/hello.cfm view file, save your file and call the say/hello action from your browser. Your browser should look like Figure 6 below.

You can see that Wheels created a link for us and added an appropriate URL for the say/goodbye action to the link.

Linking Goodbye to Hello

Let's complete our little app and add a corresponding link to the bottom of our say/goodbye.cfm view page.

Open your say/goodbye.cfm view page and modify it so it looks like the code block below.

CFML: app/views/say/goodbye.cfm

app/views/say/goodbye.cfm
<h1>Goodbye World!</h1>
<p>Time to say <cfoutput>#linkTo(text="hello", action="hello")#?</cfoutput></p>

If you now call the say/goodbye action in your browser, your browser should look like Figure 7 below.

Much More to Learn

You now know enough to be dangerous with Wheels. Look out! But there are many more powerful features to cover. You may have noticed that we haven't even talked about the M in MVC.

No worries. We will get there. And we think you will enjoy it.

CLI Commands

One module that we have created is a module that extends CommandBox itself with commands and features specific to the Wheels framework. The Wheels CLI module for CommandBox is modeled after the Ruby on Rails CLI module and gives similar capabilities to the Wheels developer.

Install CommandBox

Once installed, you can either double-click on the box executable which opens the CommandBox shell window, or run box from a CMD window in Windows, Terminal window in MacOS, or shell prompt on a Linux server. Sometimes you only want to call a single CommandBox command and don't need to launch a whole CommandBox shell window to do that, for these instances you can call the CommandBox command directly from your default system terminal window by prefixing the command with the box prefix.

So to run the CommandBox version command you could run box version from the shell or you could launch the CommandBox shell and run version inside it.

box version

version

This is a good concept to grasp, cause depending on your workflow, you may find it easier to do one versus the other. Most of the commands you will see in these CLI guides will assume that you are entering the command in the actual CommandBox shell so the box prefix is left off.

Install the wheels-cli CommandBox Module

Okay, now that we have CommandBox installed, let's add the Wheels CLI module.

install wheels-cli

Installing this module will add a number of commands to your default CommandBox installation. All of these commands are prefixed by the wheels name space. There are commands to create a brand new Wheels application or scaffold out sections of your application. We'll see some of these commands in action momentarily.

These tools allow you to adopt a more modern workflow and allow you to create and manipulate many Wheels objects from the command line. By making these tools available in the command line, not only will you be able to speed up your development but you can also utilize these commands in Continuous Integration (CI) and Continuous Deployment (CD) work flows.

Running Local Development Servers

Starting a local development server

With CommandBox, we don't need to have Lucee or Adobe ColdFusion installed locally. With a simple command, we can make CommandBox go and get the CFML engine we've requested, and quickly create a server running on Undertow. Make sure you're in the root of your website, and then run:

server start

The server will then start on a random port on 127.0.0.1 based the configuration from the server.json file that is in the root of your application that comes with wheels. We can add various options to server.json to customize our server. Your default server.json will look something like this:

In this server.json file, the server name is set to wheels, meaning I can now start the server from any directory by simply calling start myApp. We don't have any port specified, but you can specify any port you want. Lastly, we have URL rewriting enabled and pointed the URL rewrite configuration file to public/urlrewrite.xml, which is included starting from Wheels 2.x.

Using custom host names

Controlling local servers

Obviously, anything you start, you might want to stop. Servers can be stopped either via right/ctrl clicking on the icon in the taskbar, or by the stop command. To stop a server running in the current directory issue the following:

server stop

You can also stop a server from anywhere by using its name:

server stop myApp

If you want to see what server configurations exist on your system and their current status, simply do server list

server list

To remove a server configuration from the list, you can use server forget myapp. Note the status of the servers on the list are somewhat unreliable, as it only remembers the last known state of the server: so if you start a server and then turn off your local machine, it may still remember it as running when you turn your local machine back on, which is why we recommend the use of force: true in the server.json file.

Specifying different CF engines

By default, CommandBox will run Lucee (version 6.x at time of writing). You may wish to specify an exact version of Lucee, or use Adobe ColdFusion. We can do this via either setting the appropriate cfengine setting in server.json, or at runtime with the cfengine= argument.

Start the default engine

CommandBox> start

__

Start the latest stable Lucee 5.x engine

CommandBox> start cfengine=lucee@5

__

Start a specific engine and version

CommandBox> start cfengine=adobe@10.0.12

__

Start the most recent Adobe server that starts with version "11"

CommandBox> start cfengine=adobe@11

__

Start the most recent adobe engine that matches the range

CommandBox> start cfengine="adobe@>9.0 <=11"

Or via server.json

CFIDE / Lucee administrators

The default username and password for all administrators is admin & commandbox

You can of course run multiple servers, so if you need to test your app on Lucee 5.x, Lucee 6.x and Adobe 2018, you can just start three servers with different cfengine= arguments!

Watch out

CommandBox 5.1 required to install dependencies easily

By default, the Lucee server that CommandBox starts includes all the essential Lucee extensions you need, but if need to minimize the size of the Lucee instance you launch, then you can use Lucee-Light by specifying cfengine=lucee-light in your server.json file. Wheels can run just fine on lucee-light (which is after all, Lucee, minus all the extensions) but at a minimum, requires the following extensions to be installed as dependencies in your box.json. Please note you may have to add any drivers you need for your database to this list as well.

Once added to your box.json file, while the server is running, just do box install, which will install the dependencies, and load them into the running server within 60 seconds.

Alternatively you can download the extensions and add them manually to your server root's deploy folder (i.e \WEB-INF\lucee-server\deploy)

Screencasts

Tutorials, demonstrations, and presentations about the ColdFusion on Wheels framework.

Wheels 2.x

Wheels 1.x

Please note that all the webcasts below were created with Wheels 1.x in mind, and are listed here as they might still be useful to those starting out.

CRUD series

"Building a Social Network"

Chris Peters adds data validation to the user registration form

Other

Manual Installation

Instructions for installing Wheels on your system.

Installing Wheels is so simple that there is barely a need for a chapter devoted to it. But we figured we'd better make one anyway in case anyone is specifically looking for a chapter about installation.

So, here are the simple steps you need to follow to get rolling on Wheels...

Manual Installation

1. Download Wheels

You have 2 choices when downloading Wheels. You can either use the latest official release of Wheels, or you can take a walk on the wild side and go with the latest committed source code in our Git repository.

In most cases, we recommend going with the official release because it's well documented and has been through a lot of bug testing. Only if you're in desperate need of a feature that has not been released yet would we advise you to go with the version stored in the Git master branch.

Let's assume you have downloaded the latest official release. (Really, you should go with this option.) You now have a .zip file saved somewhere on your computer. On to the next step...

2. Setup the Website

Getting an empty website running with Wheels installed is an easy process if you already know your way around IIS or Apache. Basically, you need to create a new website in your web server of choice and unzip the contents of the file into the root of it.

  • Create a new folder under your web root (usually C:\Inetpub\wwwroot) named wheels_site and unzip the Wheels .zip file into the root of it.

  • Create a new website using IIS called Wheels Site with localhost as the host header name and C:\Inetpub\wwwroot\mysite as the path to your home directory.

3. Setup the Database (Optional)

Create a new database in MySQL, PostgreSQL, Microsoft SQL Server, or H2 and add a new data source for it in the ColdFusion/Lucee Administrator, just as you'd normally do. Now open up app/config/settings.cfm and call set(dataSourceName="") with the name you chose for the data source.

4. Test It

When you've followed the steps above, you can test your installation by typing http://localhost/ (or whatever you set as the host header name) in your web browser. You should get a "Congratulations!" page.

That's it. You're done. This is where the fun begins!

Upgrading

Instructions for upgrading Wheels applications

Generally speaking, upgrading Wheels is as easy as replacing the wheels folder, especially for those small maintenance releases: however, there are usually exceptions in minor point releases (i.e, 1.1 to 1.3 required replacing other files outside the wheels folder). The notes below detail those changes.

Upgrading to 3.0.0

Compatibility Changes

Adobe Coldfusion 2016 and below are no longer compatible with Wheels going forward. Consequently, these versions have been removed from the Wheels Internal Test Suites.

Code changes

  • After installing Wheels 3.x, you'll have to run box install to intall testbox and wirebox in your application as they are not shipped with Wheels but are rather listed in box.json file as dependencies to be installed.

  • Added Mappings for the app, vendor, wheels, wirebox, testbox and tests directories.

  • root.cfm and rewrite.cfm have been removed. All the requests are now being redirected only through public/index.cfm.

Changes to the wheels folder

  • Replace the wheels folder with the new one from the 3.0.0 download.

  • Move the wheels folder inside the vendor folder.

Changes outside the wheels folder

  • Moved the config, controllers, events, global, lib, migrator, models, plugins, snippets and views directories inside the app directory.

  • Moved the files, images, javascripts, miscellaneous, stylesheets directories and Application.cfc, index.cfm and urlrewrite.xml files into the public folder.

Upgrading to 2.3.x

Replace the wheels folder with the new one from the 2.3.0 download.

Upgrading to 2.2.x

Replace the wheels folder with the new one from the 2.2.0 download.

Upgrading to 2.1.x

Replace the wheels folder with the new one from the 2.1.0 download.

Code changes

  • Rename any instances of findLast() to findLastOne()

Changes outside the wheels folder

  • Create /events/onabort.cfm to support the onAbort method

Upgrading to 2.0.x

As always, the first step is to replace the wheels folder with the new one from the 2.0 download.

Other major changes required to upgrade your application are listed in the following sections.

Supported CFML Engines

Wheels 2.0 requires one of these CFML engines:

  • Lucee 4.5.5.006 + / 5.2.1.9+

  • Adobe ColdFusion 10.0.23 / 11.0.12+ / 2016.0.4+

We've updated our minimum requirements to match officially supported versions from the vendors. (For example, Adobe discontinued support for ColdFusion 10 in May 2017, which causes it to be exposed to security exploits in the future. We've included it in 2.0 but it may be discontinued in a future version)

Changes outside the wheels folder

  • The events/functions.cfm file has been moved to global/functions.cfm.

  • The models/Model.cfc file should extend wheels.Model instead of Wheels (models/Wheels.cfc can be deleted).

  • The controllers/Controller.cfc file should extend wheels.Controller instead of Wheels(controllers/Wheels.cfc can be deleted).

  • The init function of controllers and models must be renamed to config.

  • The global setting modelRequireInit has been renamed to modelRequireConfig.

  • The global setting cacheControllerInitialization has been renamed to cacheControllerConfig.

  • The global setting cacheModelInitialization has been renamed to cacheModelConfig.

  • The global setting clearServerCache has been renamed to clearTemplateCache.

  • The updateProperties() method has been removed, use update() instead.

  • The renderPage function has been renamed to renderView

  • includePartial() now requires the partial and query arguments to be set (if using a query)

Routing

By default, this is limited to GET requests for security reasons.

Cross-Site Request Forgery (CSRF) Protection

It is strongly recommended that you enable Wheels 2.0's built-in CSRF protection.

For many applications, you need to follow these steps:

  1. Update your route definitions to enforce HTTP verbs on actions that manipulate data (get, post, patch, delete, etc.)

  2. Make sure that forms within the application are POSTing data to the actions that require post, patch, and delete verbs.

Database Migrations

If you have previously been using the dbmigrate plugin, you can now use the inbuilt version within the Wheels 2 core.

Database Migration files in /db/migrate/ should now be moved to /migrator/migrations and extend wheels.migrator.Migration, not plugins.dbmigrate.Migration which can be changed with a simple find and replace. Note: Oracle is not currently supported for Migrator.

Upgrading to 1.4.x

  1. Replace the wheels folder with the new one from the 1.4 download.

  2. Replace URL rewriting rule files – i.e, .htaccess, web.config, IsapiRewrite.ini

In addition, if you're upgrading from an earlier version of Wheels, we recommend reviewing the instructions from earlier reference guides below.

Upgrading to 1.3.x

If you are upgrading from Wheels 1.1.0 or newer, follow these steps:

  1. Replace the wheels folder with the new one from the 1.3 download.

  2. Replace the root root.cfm file with the new one from the 1.3 download.

  3. Remove the <cfheader> calls from the following files:

    • events/onerror.cfm

    • events/onmaintenance.cfm

    • events/onmissingtemplate.cfm

In addition, if you're upgrading from an earlier version of Wheels, we recommend reviewing the instructions from earlier reference guides below.

Note: To accompany the newest 1.1.x releases, we've highlighted the changes that are affected by each release in this cycle.

Upgrading to 1.1.x

If you are upgrading from Wheels 1.0 or newer, the easiest way to upgrade is to replace the wheels folder with the new one from the 1.1 download. If you are upgrading from an earlier version, we recommend reviewing the steps outlined in Upgrading to Wheels 1.0.

Note: To accompany the newest 1.1.x releases, we've highlighted the changes that are affected by each release in this cycle.

Plugin Compatibility

Be sure to review your plugins and their compatibility with your newly-updated version of Wheels. Some plugins may stop working, throw errors, or cause unexpected behavior in your application.

Supported System Changes

  • 1.1: The minimum Adobe ColdFusion version required is now 8.0.1.

  • 1.1: The minimum Railo version required is now 3.1.2.020.

  • 1.1: The H2 database engine is now supported.

File System Changes

  • 1.1: The .htaccess file has been changed. Be sure to copy over the new one from the new version 1.1 download and copy any addition changes that you may have also made to the original version.

Database Structure Changes

  • 1.1: By default, Wheels 1.1 will wrap database queries in transactions. This requires that your database engine supports transactions. For MySQL in particular, you can convert your MyISAM tables to InnoDB to be compatible with this new functionality. Otherwise, to turn off automatic transactions, place a call to set(transactionMode="none").

  • 1.1: Binary data types are now supported.

CFML Code Changes

Model Code

  • 1.1: Validations will be applied to some model properties automatically. This may cause unintended behavior with your validations. To turn this setting off, call set(automaticValidations=false) in config/settings.cfm.

  • 1.1: The class argument in hasOne(), hasMany(), and belongsTo() has been deprecated. Use the modelName argument instead.

  • 1.1: afterFind() callbacks no longer require special logic to handle the setting of properties in objects and queries. (The "query way" works for both cases now.) Because arguments will always be passed in to the method, you can't rely on StructIsEmpty() to determine if you're dealing with an object or not. In the rare cases that you need to know, you can now call isInstance() or isClass() instead.

  • 1.1: On create, a model will now set the updatedAt auto-timestamp to the same value as the createdAt timestamp. To override this behavior, call set(setUpdatedAtOnCreate=false) in config/settings.cfm.

View Code

  • 1.1: Object form helpers (e.g. textField() and radioButton()) now automatically display a label based on the property name. If you left the label argument blank while using an earlier version of Wheels, some labels may start appearing automatically, leaving you with unintended results. To stop a label from appearing, use label=false instead.

  • 1.1: The contentForLayout() helper to be used in your layout files has been deprecated. Use the includeContent() helper instead.

  • 1.1: In production mode, query strings will automatically be added to the end of all asset URLs (which includes JavaScript includes, stylesheet links, and images). To turn off this setting, call set(assetQueryString=false) in config/settings.cfm.

  • 1.1: stylesheetLinkTag() and javaScriptIncludeTag() now accept external URLs for the source/sources argument. If you manually typed out these tags in previous releases, you can now use these helpers instead.

  • 1.1: flashMessages(), errorMessageOn(), and errorMessagesFor() now create camelCased class attributes instead (for example error-messages is now errorMessages). The same goes for the class attribute on the tag that wraps form elements with errors: it is now fieldWithErrors.

Controller Code

  • 1.1.1: The if argument in all validation functions is now deprecated. Use the condition argument instead.

Upgrading to 1.0.x

Our listing of steps to take while upgrading your Wheels application from earlier versions to 1.0.x.

Upgrading from an earlier version of 1.x? Then the upgrade path is simple. All you need to do is replace the wheels folder with the new wheels folder from the download.

The easiest way to upgrade is to setup an empty website, deploy a fresh copy of Wheels 1.0, and then transfer your application code to it. When transferring, please make note of the following changes and make the appropriate changes to your code.

Note: To accompany the newest 1.0 release, we've highlighted the changes that are affected by that release.

Supported System Changes

  • 1.0: URL rewriting with IIS 7 is now supported.

  • 1.0: URL rewriting in a sub folder on Apache is now supported.

  • ColdFusion 9 is now supported.

  • Oracle 10g or later is now supported.

  • PostgreSQL is now supported.

  • Railo 3.1 is now supported.

File System Changes

  • 1.0: There is now an app.cfm file in the config folder. Use it to set variables that you'd normally set in Application.cfc (i.e., this.name, this.sessionManagement, this.customTagPaths, etc.)

  • 1.0: There is now a web.config file in the root.

  • 1.0: There is now a Wheels.cfc file in the models folder.

  • 1.0: The Wheels.cfc file in the controllers folder has been updated.

  • 1.0: The IsapiRewrite4.ini and .htaccess files in the root have both been updated.

  • The controller folder has been changed to controllers.

  • The model folder has been changed to models.

  • The view folder has been changed to views.

  • Rename all of your CFCs in models and controllers to UpperCamelCase. So controller.cfc will become Controller.cfc, adminUser.cfc will become AdminUser.cfc, and so on.

  • All images must now be stored in the images folder, files in the files folder, JavaScript files in the javascripts folder, and CSS files in the stylesheets folder off of the root.

Database Structure Changes

  • deletedOn, updatedOn, and createdOn are no longer available as auto-generated fields. Please change the names to deletedAt, updatedAt, and createdAt instead to get similar functionality, and make sure that they are of type datetime, timestamp, or equivalent.

CFML Code Changes

Config Code

  • 1.0: The action of the default route (home) has changed to wheels. The way configuration settings are done has changed quite a bit. To change a Wheels application setting, use the new set() function with the name of the Wheels property to change. (For example, <cfset set(dataSourceName="mydatasource")>.) To see a list of available Wheels settings, refer to the Configuration and Defaults chapter. Model Code

  • 1.0: The extends attribute in models/Model.cfc should now be Wheels.

  • findById() is now called findByKey(). Additionally, its id argument is now named key instead. For composite keys, this argument will accept a comma-delimited list.

  • When using a model's findByKey() or findOne() functions, the found property is no longer available. Instead, the functions return false if the record was not found.

  • A model's errorsOn() function now always returns an array, even if there are no errors on the field. When there are errors for the field, the array elements will contain a struct with name, fieldName, and message elements.

  • The way callbacks are created has changed. There is now a method for each callback event ( beforeValidation(), beforeValidationOnCreate(), etc.) that should be called from your model's init() method. These methods take a single argument: the method within your model that should be invoked during the callback event. See the chapter on Object Callbacks for an example.

View Code

  • 1.0: The contents of the views/wheels folder have been changed.

  • The wrapLabel argument in form helpers is now replaced with labelPlacement. Valid values for labelPlacement are before, after, and around.

  • The first argument for includePartial() has changed from name to partial. If you're referring to it through a named argument, you'll need to replace all instances with partial.

  • The variable that keeps a counter of the current record when using includePartial() with a query has been renamed from currentRow to current.

  • There is now an included wheels view folder in views. Be sure to copy that into your existing Wheels application if you're upgrading.

  • The location of the default layout has changed. It is now stored at /views/layout.cfm. Now controller-specific layouts are stored in their respective view folder as layout.cfm. For example, a custom layout for www.domain.com/about would be stored at /views/about/layout.cfm.

  • In linkTo(), the id argument is now called key. It now accepts a comma-delimited list in the case of composite keys.

  • The linkTo() function also accepts an object for the key argument, in which case it will automatically extract the keys from it for use in the hyperlink.

  • The linkTo() function can be used only for controller-, action-, and route-driven links now. * The url argument has been removed, so now all static links should be coded using a standard "a" tag.

Controller Code

URL/Routing

  • The default route for Wheels has changed from [controller]/[action]/[id] to [controller]/[action]/[key]. This is to support composite keys. The params.id value will now only be available as params.key.

  • You can now pass along composite keys in the URL. Delimit multiple keys with a comma. (If you want to use this feature, then you can't have a comma in the key value itself).

Simply the best web development language in the world! The best way to learn it, in our humble opinion, is to get the free developer edition of Adobe ColdFusion, buy Ben Forta's ColdFusion Web Application Construction Kit series, and start coding using your programming editor of choice. Remember it's not just the commercial Adobe offering that's available; offers an excellent open source alternative. Using is a great and simple way to get a local development environment of your choice up and running quickly.

2018 / 2021 / 2023

5.2.1.9+ / 6

You also need a web server. Wheels runs on all popular web servers, including Apache, Microsoft IIS, Jetty, and the JRun or Tomcat web server that ships with Adobe ColdFusion. Some web servers support URL rewriting out of the box, some support the cgi.PATH_INFO variable which is used to achieve partial rewriting, and some don't have support for either. For local development, we strongly encourage the use of .

Don't worry though. Wheels will adopt to your setup and run just fine, but the URLs that it creates might differ a bit. You can read more about this in the chapter.

If you're using MySQL 5.7.5+ you should be aware that the ONLY_FULL_GROUP_BY setting is enabled by default and it's currently not compatible with the Wheels ORM. However, you can work around this by either disabling the ONLY_FULL_GROUP_BY setting or using ANY_VALUE() in a calculated property. You can read more about it .

We also recommend using the InnoDB engine if you want to work.

You can download all the source code for this sample application from

These are database used by Wheels. This framework strongly encourages that everyone follow convention over configuration. That way everyone is doing things mostly the same way, leading to less maintenance and training headaches down the road.

To generate the form tag's action attribute, the function takes parameters similar to the function that we introduced in the Beginner Tutorial: Hello World tutorial. We can pass in controller, action, key, and other route- and parameter-defined URLs just like we do with .

To end the form, we use the function. Easy enough.

The and helpers are similar. As you probably guessed, they create <input> elements with type="text" and type="password", respectively. And the function creates an <input type="submit" /> element.

One thing you'll notice is the and functions accept arguments called objectName and property. As it turns out, this particular view code will throw an error because these functions are expecting an object named user. Let's fix that.

As it turns out, our controller needs to provide the view with a blank user object (whose instance variable will also be called user in this case). In our new action, we will use the function to generate a new instance of the user model.

To get a blank set of properties in the model, we'll also call the generated model's method.

A basic way of doing this is using the model object's method:

Notice that our create action above redirects the user to the users index route using the function. We'll use this action to list all users in the system with "Edit" links. We'll also provide a link to the "New User" form that we just coded.

This call to the model's method will return a query object of all users in the system. By using the method's order argument, we're also telling the database to order the records by username.

To update the user, simply call its method with the user struct passed from the form via params. It's that simple.

After the update, we'll add a success message and send the end user back to the edit form in case they want to make more changes.

We simply load the user using the model's method and then call the object's method. That's all there is to it.

While there are several flavors of JavaScript libraries out there with AJAX support, we will be using the in this tutorial. Let's assume that you are fairly familiar with the basics of jQuery and know how to set it up.

But let's make it into an asynchronous request. Add this JavaScript (either on the page inside script tags or in a separate .js file included via ):

The last thing that we need to do is implement the say/hello action. Note that the request expects a dataType of JSON. By default, Wheels controllers only generate HTML responses, but there is an easy way to generate JSON instead using Wheels's and functions:

In this controller's config() method, we use the function to indicate that we want all actions in the controller to be able to respond with the data in HTML or JSON formats. Note that the client calling the action can request the type by passing a URL parameter named format or by sending the format in the request header.

The call to in the hello action takes care of the translation to the requested format. Our JavaScript is requesting JSON, so Wheels will format the greeting struct as JSON automatically and send it back to the client. If the client requested HTML or the default of none, Wheels will process and serve the view template at app/views/say/hello.cfm. For more information about and , reference the chapter on .

Let's make sure we're all on the same page. I'm going to assume that you've followed the guide and have CommandBox all setup. If you haven't done that, stop and read that guide get everything setup. It's okay, this web page will wait for you.

So what happens if we try to call our new controller right now? Lets take a look. Open your browser and point your browser to the new controller. Because my local server is installed on port 60000, my URL is http://127.0.0.1:60000/say. You may need to enter a different URL, depending on how your web server is configured. In my case, I'm using .

Figure 2: Wheels error after setting up your blank say controller
Figure 3: Your first working Wheels action.
Figure 4: Hello World with the current date and time
Figure 5: Your new goodbye action

The function is a built-in Wheels function. In this case, we are passing 2 named parameters to it. The first parameter, text, is the text that will be displayed in the hyperlink. The second parameter, action, defines the action to point the link to. By using this built-in function, your application's main URL may change, and even controllers and actions may get shifted around, but you won't suffer from the dreaded dead link. Wheels will always create a valid link for you as long as you configure it correctly when you make infrastructure changes to your application.

Figure 6: Your say/hello action with a link to the goodbye action
Figure 7: Your say/goodbye action with a link to the hello action

The command line tools extends the functionality of with some commands specifically designed for Wheels development.

brings a whole host of command line capabilities to the CFML developer. It allows you to write scripts that can be executed at the command line written entirely in CFML. It allows you to start a CFML server from any directory on your machine and wire up the code in that directory as the web root of the server. What's more is, those servers can be either Lucee servers or Adobe ColdFusion servers. You can even specify what version of each server to launch. Lastly, CommandBox is a package manager for CFML. That means you can take some CFML code and package it up into a module, host it on ForgeBox.io, and make it available to other CFML developers. In fact we make extensive use of these capabilities to distribute Wheels plugins and templates. More on that later.

The first step is to get downloaded and running. CommandBox is available for Windows, Mac & Linux, and can be installed manually or using one of the respective package managers for each OS. You can use on Windows, on MacOS, or Yum/Apt on Linux depending on your flavor of Linux. Please follow the instructions on how to install CommandBox on your particular operating system. At the end of the installation process you want to make sure the box command is part of your system path so you can call the command from any directory on your system.

You can also specify hosts other than localhost: there's a useful CommandBox module to do that () which will automatically create entries in your hosts file to allow for domains such as myapp.local running on port 80. You can install it via install commandbox-hostupdater when running the box shell with administrator privileges.

Create a basic CRUD interface in Wheels 2.x

Create a basic JSON API in Wheels 2.x

Routing in Wheels 2.x - Part 1

Routing in Wheels 2.x - Part 2

Introduction to Unit Testing in Wheels 2.x

Unit Testing Controllers in Wheels 2.x

Learn about basic create operations when building standard CRUD functionality in Wheels

Learn about basic read operations when building standard CRUD functionality in Wheels

Chris Peters demonstrates updating data in a simple CRUD Wheels application

Learn how simple it is to delete records in a basic CRUD application using Wheels

Chris Peters starts the webcast series by demonstrating how to set up ColdFusion on Wheels

Chris Peters demonstrates how to bind a Wheels model object to a form through the use of form helpers

Chris Peters finishes the "success" portion of the registration functionality by adding a success message to the Flash and redirecting the user to their home screen

Chris Peters teaches you about more validation options and how you can add them to the registration form quickly and easily

Chris Peters stylizes form markup globally using a Wheels feature called global helpers

Learn how to set up simple user authentication on a website by using a Wheels feature called filters

Learn the mechanics of reading a single record from the database and displaying its data in the view

Creating custom URL patterns is a breeze in ColdFusion on Wheels

Learn how to fetch multiple records from your model with findAll() and then display them to the user using ColdFusion on Wheels

Learn how to factor out logic in your view templates into custom helper functions in ColdFusion on Wheels

Chris Peters demonstrates joining data together with model associations using ColdFusion on Wheels

All it takes to offer pagination is two extra arguments to findAll() and a call to a view helper called paginationLinks()

Learn how to use the provides() and renderWith() functions to automatically serialize data into XML, JSON, and more

Peter Amiri walks you through setting up a "Hello World" application using the ColdFusion on Wheels framework

Chris Peters gives a high level overview of the ORM included with ColdFusion on Wheels

Chris Peters from Liquifusion demonstrates the ColdRoute plugin for Wheels

Doug Boude demonstrates using his new Wirebox plugin for Wheels

Chris Peters from Liquifusion demonstrates creating tables and records in the DBMigrate plugin for ColdFusion on Wheels

Online ColdFusion Meetup (coldfusionmeetup.com) session for March 10 2011, "What's New in Wheels 1.1", with Chris Peters:

A quick demo of the Wheels Textmate bundle by Russ Johnson

The latest official releases can always be found in the section of GitHub, and the Git repository is available at our .

In case you're not sure, here are the instructions for setting up an empty Wheels site that can be accessed when typing localhost in your browser. The instructions refer to a system running Windows Server 2003 and IIS, but you should be able to follow along and apply the instructions with minor modifications to your system. (See for a list of tested systems).

If you want to run a Wheels-powered application from a subfolder in an existing website, this is entirely possible, but you may need to get a little creative with your URL rewrite rules if you want to get pretty URLs--it will only work out of the box on recent versions of Apache. (Read more about this in the chapter.)

If you don't want to be bothered by opening up a Wheels configuration file at all, there is a nice convention you can follow for the naming. Just name your data source with the same name as the folder you are running your website from (mysite in the example above), and Wheels will use that when you haven't set the dataSourceName setting using the function.

Wheels follows Semantic Versioning () so large version changes (e.g, 1.x.x -> 2.x.x) will most likely contain breaking changes which will require evaluation of your codebase. Minor version changes (e.g, 1.3.x->1.4.x) will often contain new functionality, but in a backwards-compatible manner, and maintenance releases (e.g 1.4.4 -> 1.4.5) will just be trying to fix bugs.

Migrate your tests from the tests directory which are written with rocketUnit and rewrite them into in the tests/Testbox directory. Starting with Wheels 3.x, will replace RocketUnit as the default testing framework.

Starting with Wheels 3.x, will be used as the default dependency injector.

A .env file has been added in the root of the application which adds the H2 database extension for lucee and sets the cfadmin password to commandbox for both and .

JavaScript arguments like confirm and disable have been removed from the link and form helper functions (use the and plugins to reinstate the old behavior).

The function has been removed in Wheels 2.0 in favor of a new routing API. See the chapter for information about the new RESTful routing system.

A limited version of the "wildcard" route ([controller]/[action]/[key]) is available as [controller]/[action]) if you use the new mapper method:

In controllers/Controller.cfc, add a call to to the config method.

Add a call to the helper in your layouts' <head> sections.

Configure any AJAX calls that POST data to your application to pass the authenticityToken from the <meta>tags generated by as an X-CSRF-TOKEN HTTP header.

See documentation for the for more information.

Note: If you had previously installed the , you may remove it and rely on the functionality included in the Wheels 2 core.

1.0: The extends attribute in controllers/Controller.cfc should now be Wheels. Multiple-word controllers and actions are now word-delimited by hyphens in the URL. For example, if your controller is called SiteAdmin and the action is called editLayout, the URL to access that action would be .

Lucee
CommandBox
Adobe ColdFusion
Lucee
CommandBox
URL Rewriting
here
Transactions
https://github.com/dhgassoc/Cfwheels-Beginner-Tutorial-Hello-Database
conventions
startFormTag()
linkTo()
linkTo()
endFormTag()
textField()
passwordField()
submitTag()
textField()
passwordField()
model()
new()
create()
redirectTo()
findAll()
update()
using the Flash
findByKey()
delete()
jQuery framework
javaScriptIncludeTag()
provides()
renderWith()
provides()
renderWith()
provides()
renderWith()
Responding with Multiple Formats
Getting Started
CommandBox
linkTo()
{
    "name":"wheels",
    "web":{
        "host":"localhost",
        "webroot":"public",
        "rewrites":{
            "enable":true,
            "config":"public/urlrewrite.xml"
        }
    },
    "app":{
        "cfengine":"lucee"
    }
}
myapp (stopped)
 http://127.0.0.1:60000
 Webroot: /Users/wheels/Documents/myapp

myAPI (stopped)
 http://127.0.0.1:60010
 Webroot: /Users/wheels/Documents/myAPI

megasite (stopped)
 http://127.0.0.1:61280
 CF Engine: lucee 4.5.4+017
 Webroot: /Users/wheels/Documents/megasite

awesomesite (stopped)
 http://127.0.0.1:60015
 CF Engine: lucee 4.5.4+017
 Webroot: /Users/wheels/Documents/awesomeo
{
    "name":"myApp",
    "force":true,
    "web":{
        "http":{
            "host":"localhost",
            "port":60000
        },
        "rewrites":{
            "enable":true,
            "config":"urlrewrite.xml"
        }
    },
    "app":{
        "cfengine":"adobe@2018"
    },
}
"dependencies":{
    "lucee-image":"lex:https://ext.lucee.org/lucee.image.extension-1.0.0.35.lex",
    "lucee-zip": "lex:https://ext.lucee.org/compress-extension-1.0.0.2.lex",
    "lucee-esapi": "lex:https://ext.lucee.org/esapi-extension-2.1.0.18.lex"
}
app/config/routes.cfm
mapper()
    .wildcard()
.end();

wheels - commands

These are the top level commands in the wheels namespace.

wheels info

This command is the most basic of the commands and other than printing some pretty ASCII art it also displays the Current Working Directory, the CommandBox Module Root which can be handy when trying to diagnose version discrepancies, and lastly the Wheels version currently installed. The version is determined from a variety of sources. First and foremost, if there is a box.json file in the vendor/wheels/ directory the version is extracted from that box.json. Alternatively, if there is no box.json file in the wheels/ directory, we look in vendor/wheels/events/onapplicationstart.cfm and extract a version number from that file. That is the version number that is displayed on the default congratulations screen by the way. If both of these fail to get us a version number we can use, we ask you to let us know what version of wheels you are using and give you the option of generating a box.json file. This is handy for bringing old legacy installations under CLI control.

 ,-----.,------.,--.   ,--.,--.                   ,--.            ,-----.,--.   ,--.
'  .--./|  .---'|  |   |  ||  ,---.  ,---.  ,---. |  | ,---.     '  .--./|  |   |  |
|  |    |  `--, |  |.'.|  ||  .-.  || .-. :| .-. :|  |(  .-'     |  |    |  |   |  |
'  '--'\|  |`   |   ,'.   ||  | |  |\   --.\   --.|  |.-'  `)    '  '--'\|  '--.|  |
 `-----'`--'    '--'   '--'`--' `--' `----' `----'`--'`----'      `-----'`-----'`--'
=================================== Wheels CLI ===================================
Current Working Directory: /Users/peter/projects/ws/MyWheelsApp/
CommandBox Module Root: /Users/peter/projects/wheels-cli/
Current Wheels Version in this directory: 3.0.0
====================================================================================

wheels init

This will attempt to bootstrap an EXISTING wheels app to work with the CLI.

We'll assume the database/datasource exists and the other config options like reloadpassword is all set up. If there's no box.json, create it, and ask for the version number if we can't determine it. If there's no server.json, create it, and ask for cfengine preferences. We'll ignore other templating objects for now, as this will probably be in place too.

wheels reload

This command will reload your Wheels application. In order for this command to work, your local server needs to be started and running. This command basically issues a request to the running Wheels application to reload as if you were doing it from your browsers address bar. You will be prompted for your reload password that will be passed to the reload endpoint.

wheels reload [mode]
Parameter
Required
Default
Description

mode

false

development

possible values development, testing, maintenance, production

wheels test

This command will call the Test Runner in a running server and return the results. The command can be run from within the directory of the running server or you can specify the server name to run against. Additionally you can specify the test suite to run, possible choices include the running application's test suite (app), the core framework test suite (core), or a particular plugin's test suite by passing in the plugin name.

wheels test [type] [servername] [reload] [debug]
Parameter
Required
Default
Description

type

false

app

Either Core, App, or the name of the plugin

servername

false

Servername to run the tests against

reload

false

false

Force Reload

debug

false

false

Output passing tests as well as failing ones

format

false

json

Force a specific return format for debug

adapter

false

Attempt to override what dbadapter wheels uses

wheels scaffold

This command will completely scaffold a new object. Typically you would run this command to stub out all the CRUD related files and then follow it up with a series of wheels g property commands to add the individual fields to the object. This command will:

  • Create a model file

  • A Default CRUD Controller complete with create/edit/update/delete code

  • View files for all those actions

  • Associated test stubs

  • DB migration file

This command can be run without the server running except the database migration portion because that requires a running database. So if your server is already up and running you can run this command completely including the database migration portion. Afterwards make sure to run wheels reload to reload your application since we just made model changes. If the server isn't running, you can run this command and stub out all the files, then start your server with server start and finally migrate the database with wheels db latest.

wheels scaffold [objectName]
Parameter
Required
Default
Description

Name

true

Name of the object to scaffold out

wheels destroy

This command will destroy a given object. This is highly destructive, given the name, so proceed with caution. If you created an object using the wheels scaffold [objectName] command, this command is the inverse of that command and will remove all elements created by that command.

This command will destroy the following elements:

  • the models definition file

  • the controllers definition file

  • the view sub folder and all it's contents

  • the model test file

  • the controller test file

  • the views test file

  • resource route configuration

wheels destroy [objectName]
Parameter
Required
Default
Description

Name

true

Name of the object to destroy

wheels dbmigrate - commands

These are the commands in the wheels dbmigrate namespace. They allow you to manipulate the database structure and script the changes. This makes is easy to share your changes with coworkers, check them into source control, or apply them automatically when you deploy your code to production.

wheels dbmigrate

This is another namespace with sub commands within it. It also has an alias of wheels db which allows you to shorten the command you need to type.

wheels dbmigrate info

This command doesn't take any inputs but simply tries to communicate with the running server and gather information about your migrations and displays them in a table.

wheels dbmigrate info
❯ wheels db info
Sending: http://127.0.0.1:59144/?controller=wheels&action=wheels&view=cli&command=info
Call to bridge was successful.
+-----------------------------------------+-----------------------------------------+
| Datasource:                  wheels.dev | Total Migrations:                     3 |
| Database Type:                       H2 | Available Migrations:                 2 |
|                                         | Current Version:         20220619110404 |
|                                         | Latest Version:          20220619110706 |
+-----------------------------------------+-----------------------------------------+
+----------+------------------------------------------------------------------------+
|          | 20220619110706_cli_create_column_user_lastname                         |
|          | 20220619110540_cli_create_column_user_firstname                        |
| migrated | 20220619110404_cli_create_table_users                                  |
+----------+------------------------------------------------------------------------+

From the information presented in the two tables you can see how many migration files are in your application and of those how many have already been applied and available to be applied. You are also presented with the datasource name and database type information.

wheels dbmigrate latest

This command will migrate the database to the latest version. This command will apply each migration file from the current state all the way to the latest one at a time. If a SQL error is encountered in one of the files, the command will stop at that point and report the error.

wheels dbmigrate latest

wheels dbmigrate reset

This command will migrate the database to version 0 effectively resetting the database to nothing.

wheels dbmigrate reset

wheels dbmigrate up

This command will process the next migration file from the current state. If the database is already at the latest version this command will have no effect.

wheels dbmigrate up

wheels dbmigrate down

This command will reverse the current migration file and take the database one step backwards. If the database is already at version 0 then this command will have no effect.

wheels dbmigrate down

wheels dbmigrate exec

This command will run a particular migration file and take the database to that version. The four directional migration commands above latest, reset, up, and down each in turn call this command to process their intended action.

wheels dbmigrate exec [version]
Parameter
Required
Default
Description

version

true

The version to migrate the database to

wheels dbmigrate create table

This command will generate a new migration file for creating a table in the database. Keep in mind you will still have to run the migration file but this will add it to the migration history.

wheels dbmigrate create table [name] [force] [id] [primaryKey]
Parameter
Required
Default
Description

name

true

The name of the database table to create

force

false

false

Force the creation of the table

id

false

true

Auto create ID column as autoincrement ID

primaryKey

false

ID

Overrides the default primary key column name

wheels dbmigrate create column

This command will generate a new migration file for adding a new column to an existing table.

wheels dbmigrate create column [name] [columnType] [columnName] [default] [null] [limit] [precision] [scale]
Parameter
Required
Default
Description

name

true

The name of the database table to modify

columnType

true

The column type to add

columnName

false

The column name to add

default

false

The default value to set for the column

null

false

true

Should the column allow nulls

limit

false

The character limit of the column

precision

false

The precision of the numeric column

scale

false

The scale of the numeric column

wheels dbmigrate create blank

wheels dbmigrate remove table

This command generates a migration file to remove a table. The migration file is will still need to be run individual but this command gets the migration generated and into the history.

wheels dbmigrate remove table [name]
Parameter
Required
Default
Description

name

true

The name of the database table to remove

Switching Environments

Environments that match your development stages.

Wheels allows you to set up different environments that match stages in your development cycle. That way you can configure different values that match what services to call and how your app behaves based on where you are in your development.

The Development environment is the most convenient one to use as you start building your application because it does not cache any data. Therefore, if you make any changes to your controllers and actions, for example, it will immediately be picked up by Wheels.

Other environment modes cache this information in order to speed up your application as much as possible. Making changes to the database in these most modes will cause Wheels to throw an error. (Although that can be avoided with a reload call. More on that later.)

The fastest environment mode in terms of page load time is the Production mode. This is what you should set your application to run in before you launch your website.

By default, all new applications will start in the Development environment which is middle-of-the-road in terms of convenience versus speed.

The 4 Environment Modes

Besides the 2 environments mentioned above, there are 2 more. Let's go through them all one by one so you can see the differences between them and choose the most appropriate one given your current stage of development.

Development

  • Shows friendly Wheels specific errors as well as regular ColdFusion errors on screen.

  • Does not email you when an error is encountered.

  • Caches controller and model initialization (the config() methods).

  • Caches the database schema.

  • Caches routes.

  • Caches image information.

Production

  • Caches everything that the Development mode caches.

  • Activates all developer controlled caching (actions, pages, queries and partials).

  • Shows your custom error page when an error is encountered.

  • Shows your custom 404 page when a controller or action is not found.

  • Sends an email to you when an error is encountered.

Testing

  • Same caching settings as the Production mode but using the error handling of the Development mode. (Good for testing an application at its true speed while still getting errors reported on screen.)

Maintenance

  • Shows your custom maintenance page unless the requesting IP address or user agent is in the exception list (set by calling set(ipExceptions="127.0.0.1") in app/config/settings.cfm or passed along in the URL as except=127.0.0.1, or as except=myuseragentstring to match against the user agent instead. Please note that if passing an exception on the URL using the except parameter, you must also provide the password parameter if a reload password has been defined. This eliminates the possibility of a rogue actor breaking out of maintenance mode by simply adding an except to the URL.

This environment mode comes in handy when you want to briefly take your website offline to upload changes or modify databases on production servers.

How to Switch Modes

You can change the current environment by modifying the app/config/environment.cfm file. After you've modified it, you need to either restart the ColdFusion service or issue a reload request. (See below for more info.)

The reload Request

Issuing a reload request is the easiest way to go from one environment to another. It's done by passing in reload as a parameter in the URL, like this:

HTTP
http://www.mysite.com/?reload=true

This tells Wheels to reload the entire framework (it will also run your code in the app/events/onapplicationstart.cfmfile), thus picking up any changes made in the app/config/environment.cfm file.

Lazy Reloading

There's also a shortcut for lazy developers who don't want to change this file at all. To use it, just issue the reload request like this instead:

HTTP
http://www.mysite.com/?reload=testing

This will make Wheels skip your app/config/environment.cfm file and just use the URL value instead (testing, in this case).

Password-Protected Reloads

For added protection, you can set the reloadPassword variable in app/config/settings.cfm. When set, a reload request will only be honored when the correct password is also supplied, like this:

HTTP
http://www.mysite.com/?reload=testing&password=mypass

Don't forget your reload password in production

You really don't want random users hitting ?reload=development on a production server, as it could potentially expose data about your application and error messages. Always set a reload password!

Disabling Environment Switching

If you're deploying to a container based environment, or one that you know you'll never want to switch out of production mode, you can disable URL based environment switching completely via: set(allowEnvironmentSwitchViaUrl = false);

This is just an additional check to ensure that your production mode acts in the way you expect! Application reloading is still allowed, but the configuration can not be altered.\

CommandBox
CommandBox
CommandBox
Chocolatey
Homebrew
wheels - commands
wheels generate - commands
wheels dbmigrate - commands
wheels plugins - commands
Host updater
https://youtu.be/K5HLItTru1g
https://youtu.be/qZr5JzO0vo4
https://youtu.be/BnPGApAvMVQ
https://youtu.be/0CiGxJyJEIQ
https://youtu.be/XgMuzzmBQ98
https://youtu.be/cygj9WDqHjY
View all screencasts on Vimeo
Episode 1: "C" Is for "Create" - Basic CRUD
Episode 2: "R"; Is for "Read" - Basic CRUD
Episode 3: "U" Is for "Update" - Basic CRUD
Episode 4: "D" Is for Delete - Basic CRUD
Episode 1: Setting up ColdFusion on Wheels
Episode 2: Form Helpers
Episode 3: Object Validation and Showing Errors
Episode 4: Redirects and the Flash
Episode 5: Object Validation
Episode 6: Styling Forms
Episode 7: Authentication with Filters
Episode 8: Reading and Displaying a Single Record
Episode 9: Adding a Route for User Profiles
Episode 10: Displaying Sets of Records
Episode 11: Custom View Helpers
Episode 12: Joining Models with Associations
Episode 13: Pagination
Episode 14: Responding with Multiple Formats
Hello World
CFUnited 2010: Simplifying Database Code with the ColdFusion on Wheels ORM
ColdRoute Plugin
Wirebox Plugin for Wheels
DBMigrate Create Operations
CF Meetup, March 10 2011
Wheels Textmate Bundle Demo
Releases
GitHub repo
Requirements
URL Rewriting
Set()
http://semver.org/
Testbox
Testbox
Wirebox
Lucee
Adobe ColdFusion
JS Confirm
JS Disable
addRoute()
Routing
wildcard()
protectsFromForgery()
csrfMetaTags()
csrfMetaTags()
CSRF Protection Plugin
CSRF Protection plugin
http://www.domain.com/site-admin/edit-layout

Getting Started

Install Wheels and get a local development server running

One module that we have created is a module that extends CommandBox itself with commands and features specific to the Wheels framework. The Wheels CLI module for CommandBox is modeled after the Ruby on Rails CLI module and gives similar capabilities to the Wheels developer.

Install CommandBox

Once installed, you can either double-click on the box executable which opens the CommandBox shell window, or run box from a CMD window in Windows, Terminal window in MacOS, or shell prompt on a Linux server. Sometimes you only want to call a single CommandBox command and don't need to launch a whole CommandBox shell window to do that, for these instances you can call the CommandBox command directly from your default system terminal window by prefixing the command with the box prefix.

So to run the CommandBox version command you could run box version from the shell or you could launch the CommandBox shell and run version inside it.

box version

version

This is a good concept to grasp, cause depending on your workflow, you may find it easier to do one versus the other. Most of the commands you will see in these CLI guides will assume that you are entering the command in the actual CommandBox shell so the box prefix is left off.

Install the wheels-cli CommandBox Module

Okay, now that we have CommandBox installed, let's add the Wheels CLI module.

install wheels-cli

Installing this module will add a number of commands to your default CommandBox installation. All of these commands are prefixed by the wheels name space. There are commands to create a brand new Wheels application or scaffold out sections of your application. We'll see some of these commands in action momentarily.

Start a new Application using the Wizard

To install a new application using version 3.0, we can use the new application wizard and select Bleeding Edge when prompted to select the template to use.

wheels new

Start a New Application Using the Command Line

Now that we have CommandBox installed and extended it with the Wheels CLI module, let's start our first Wheels app from the command line. We'll look at the simplest method for creating a Wheels app and starting our development server.

wheels generate app myApp server start

A few minutes after submitting the above commands a new browser window should open up and display the default Wheels congratulations screen.

So what just happened? Since we only passed the application name myApp to the wheels generate app command, it used default values for most of its parameters and downloaded our Base template (wheels-base-template) from ForgeBox.io, then downloaded the framework core files (wheels.dev) from ForgeBox.io and placed it in the vendor/wheels directory, then configured the application name and reload password, and started a Lucee server on a random port.

A Word About Command Aliases

CommandBox commands have the capability to be called by multiple names or aliases. The command above wheels generate app can also be initiated by typing wheels g app. In fact g is an alias for generate so wherever you see a command in the CLI documentation that has generate in it you can substitute g instead.

In addition to shortening generate to g, aliases can completely change the name space as well. A command that you haven't seen yet is the wheels generate app-wizard command. This command guides the user through a series of menu options, building up all the parameters needed to customize the start of a new Wheels project. You're likely to use the wizard when starting a new Wheels application so it's good to become familiar with it.

This command has the normal alias referenced above at wheels g app-wizard but it also has an additional alias at wheels new which is the command more prevalent in the Rails community. So the three commands wheels generate app-wizard, wheels g app-wizard, and wheels new all call the same functionality which guides the user though a set of menus, collecting details on how to configure the desired app. Once all the parameters have been gathered, this command actually calls the wheels generate app command to create the actual Wheels application.

This Getting Started guide has taken you from the very beginning and gotten you to the point where you can go into any empty directory on your local development machine and start a Wheels project by issuing a couple of CLI commands. In later guides we'll explore these options further and see what else the CLI can do for us.

wheels generate - commands

These are the commands in the wheels generate namespace. These commands are called by some of the top level commands but you can use them directly to speed up your development process.

wheels generate

The wheels generate command is what CommandBox calls a namespace and contains many sub commands beneath it. It's main purpose is to isolate those sub commands so there is no name collisions. It also has an alias of wheels g which allows you to shorten the commands you have to type. However, we have opted to show all the commands with their full names in the list below.

wheels generate app-wizard

Creates a new Wheels application using our wizard to gather all the necessary information. This is the recommended route to start a new application.

This command will ask for:

  • An Application Name (a new directory will be created with this name)

  • Template to use

  • A reload Password

  • A datasource name

  • What local cfengine you want to run

  • If using Lucee, do you want to setup a local h2 dev database

  • Do you want to initialize the app as a ForgeBox module

wheels new
wheels g app-wizard
wheels generate app-wizard

All these three commands are equivalent and will call the same wizard. The wizard in turn gathers all the required data and passes it all off to the wheels generate app command to do all the heavy lifting.

Let's take a look at the wizard pages after issuing the wheels new command:

You can accept the name offered or change it to whatever name you like. We try to clean up the name and take out special characters and spaces if we need to.

You can select a template to use for your app.

The datasource is something you'll have to take care of unless you opt to use the H2 Embedded database in a Lucee server. Here you can define the datasource name if you would like to use something different than the application name.

In this step you can choose what CF Engine and version to launch. Lucee has an embedded SQL compliant database server called H2. So if you use one of the Lucee options you can specify to use the H2 database as well. Notice the last item allows you to specify the module slug or URI to a CF Engine not on the list.

On this step you are asked if you'd like to use the H2 Database, in which case we can set everything up for you, or if you would prefer to use another database engine and will take care of setting up the database yourself.

On this last step, you are asked if you want us to include a box.json file so you can eventually submit this to ForgeBox.io for sharing with the world.

This is the confirmation screen that shows all the choices you've made and gives you one last chance to bail out. If you choose to continue, the choices you've made will be sent over to the wheels g app command to do the actual app creation.

If you opted to continue, you'll see a bunch of things scroll across your screen as the various items are downloaded and configured. Eventually you will see this status screen letting you know that everything was installed properly.

wheels generate app

Create a blank Wheels app from one of our app templates or a template using a valid Endpoint ID which can come from ForgeBox, HTTP/S, git, github, etc.

By default an app named MyWheelsApp will be created in a sub directory called MyWheelsApp.

The most basic call...

wheels generate app

This can be shortened to...

wheels g app

Here are the basic templates that are available for you that come from ForgeBox

  • Wheels Base Template - Stable (default)

  • Wheels Base Template - Bleeding Edge

  • Wheels Template - HelloWorld

  • Wheels Template - HelloDynamic

  • Wheels Template - HelloPages

  • Wheels Example App

  • Wheels - TodoMVC - HTMX - Demo App

wheels create app template=base

The template parameter can also be any valid Endpoint ID, which includes a Git repo or HTTP URL pointing to a package. So you can use this to publish your own templates to use to start new projects.

wheels create app template=http://site.com/myCustomAppTemplate.zip
Parameter
Required
Default
Description

name

false

MyWheelsApp

The name of the app you want to create

template

false

base template

The name of the app template to use

directory

false

mywheelsapp/

The directory to create the app in

reloadPassword

false

ChangeMe

The reload password to set for the app

datasourceName

false

MyWheelsApp

The datasource name to set for the app

cfmlEngine

false

Lucee

The CFML engine to use for the app

setupH2

false

false

Setup the H2 database for development

init

false

false

"init" the directory as a package if it isn't already

force

false

false

Force installation into an none empty directory

wheels generate route

Adds a default resources Route to the routes table. All the normal CRUD routes are automatically added.

wheels generate route [objectname]
Parameter
Required
Default
Description

objectname

true

The name of the resource to add to the routes table

wheels generate controller

I generate a controller in the app/controllers/ directory. You can either pass in a list of actions to stub out or the standard CRUD methods will be generated.

Create a user controller with full CRUD methods

wheels generate controller user

Create a user object with just "index" and "customaction" methods

wheels generate controller user index,customaction
Parameter
Required
Default
Description

name

true

Name of the controller to create without the .cfc

actionList

false

optional list of actions, comma delimited

directory

false

if for some reason you don't have your controllers in app/controllers/

wheels generate model

This command generates a model in the app/models/ folder and creates the associated DB Table using migrations.

Create "users" table and "User.cfc" in models:

wheels generate model user

Create just "User.cfc" in models:

wheels generate model user false
Parameter
Required
Default
Description

name

true

Name of the model to create without the .cfc: assumes singular

db

false

Boolean attribute specifying if the database table should be generated as well

wheels generate property

This command generates a dbmigration file to add a property to an object and scaffold into _form.cfm and show.cfm if they exist (i.e, wheels generate property table columnName columnType).

Create the a string/textField property called firstname on the User model:

wheels generate property user firstname

Create a boolean/Checkbox property called isActive on the User model with a default of 0:

wheels generate property user isActive boolean

Create a boolean/Checkbox property called hasActivated on the User model with a default of 1 (i.e, true):

wheels generate property user isActive boolean 1

Create a datetime/datetimepicker property called lastloggedin on the User model:

wheels generate property user lastloggedin datetime

All columnType options: biginteger,binary,boolean,date,datetime,decimal,float,integer,string,text,time,timestamp,uuid

Parameter
Required
Default
Description

name

true

Table Name

columnName

false

Name of Column

columnType

false

Type of Column on of biginteger, binary, boolean, date, datetime, decimal, float,integer, string, text, time, timestamp, uuid

default

false

Default Value for column

null

false

Whether to allow null values

limit

false

character or integer size limit for column

precision

false

precision value for decimal columns, i.e. number of digits the column can hold

scale

false

scale value for decimal columns, i.e. number of digits that can be placed to the right of the decimal point (must be less than or equal to precision)

wheels generate view

This command generates a view file in the app/views/ directory when specifying the object name and the action. If a directory for the object does not exist a subdirectory will be created in the app/views/ directory and the action NAME.cfm file place into it.

Create a default file called show.cfm without a template

wheels generate view user show

Create a default file called show.cfm using the default CRUD template

wheels generate view user show crud/show
Parameter
Required
Default
Description

objectname

true

View path folder, i.e user

name

true

Name of the file to create, i.e, edit

template

false

template (used in Scaffolding) - options crud/_form,crud/edit,crud/index,crud/new,crud/show

The "crud/show" parameter of this command is referring to an existing template to be used to generate the "user/show" view. The prefix "crud" points to a templates directory in the app that holds a "crud" directory in which templates like "show" are placed. In this way the "crud/show" parameter fetches the show.txt template in the "templates/crud" directory and uses that for generating the "user/show" view.

wheels generate test

This command generates a test stub in /tests/Testbox/specs/TYPE/NAME.cfc.

wheels generate test model user
wheels generate test controller users
wheels generate test view users edit
Parameter
Required
Default
Description

type

true

Type of test to generate. Options are model, controller, and view

objectname

true

View path folder or name of object, i.e user

name

true

Name of the action/view

Configuration and Defaults

An overview of Wheels configuration and how is it used in your applications. Learn how to override a Wheels convention to make it your own.

We all love the "Convention over Configuration" motto of Wheels, but what about those two cases that pop into everyone's head? What if I want to develop in my own way? Or, What about an existing application that I need to port into Wheels? Gladly, that's what configuration and defaults are there for. Let's take a look at exactly how this is performed.

Where Configurations Happen

You will find configuration files in the app/config folder of your Wheels application. In general, most of your settings will go in app/config/settings.cfm.

How to Set Configurations

How to Access Configuration Values

CFScript
if (get("environment") == "production") {
    // Do something for production environment
}

Setting CFML Application Configurations

In CFML's standard Application.cfc, you can normally set values for your application's properties in the thisscope. Wheels still provides these options to you in the file at app/config/app.cfm.

Here is an example of what can go in app/config/app.cfm:

app/config/app.cfm
this.name = "TheNextSiteToBeatTwitter";
this.sessionManagement = false;

this.customTagPaths = ListAppend(
  this.customTagPaths,
  ExpandPath("../customtags")
);

Types of Configurations Available

There are several types of configurations that you can perform in Wheels to override all those default behaviors. In Wheels, you can find all these configuration options:

Let's take a closer look at each of these options.

Environment Settings

Not only are the environments useful for separating your production settings from your "under development" settings, but they are also opportunities for you to override settings that will only take effect in a specified environment.

The setting for the current environment can be found in app/config/environment.cfm and should look something like this:

app/config/environment.cfm
set(environment="development");

Full Listing of Environment Settings

Name
Type
Default
Description

environment

string

development

Environment to load. Set this value in app/config/environment.cfm. Valid values are development, testing, maintenance, and production.

reloadPassword

string

[empty string]

Password to require when reloading the Wheels application from the URL. Leave empty to require no password.

redirectAfterReload

boolean

Enabled in maintenance and production

Whether or not to redirect away from the current URL when it includes a reload request. This hinders accidentally exposing your application's reload URL and password in web analytics software, screenshots of the browser, etc.

ipExceptions

string

[empty string]

IP addresses that Wheels will ignore when the environment is set to maintenance. That way administrators can test the site while in maintenance mode, while the rest of users will see the message loaded in app/events/onmaintenance.cfm.

allowEnvironmentSwitchViaUrl

boolean

true

Set to false to disable switching of environment configurations via URL. You can still reload the application, but switching environments themselves will be disabled.

URL Rewriting Settings

Sometimes it is useful for our applications to "force" URL rewriting. By default, Wheels will try to determine what type of URL rewriting to perform and set it up for you. But you can force in or out this setting by using the example below:

CFScript
set(urlRewriting="Off");

The code above will tell Wheels to skip its automatic detection of the URL Rewriting capabilities and just set it as "Off".

You can also set it to "Partial" if you believe that your web server is capable of rewriting the URL as folders after index.cfm.

Data Source Settings

Probably the most important configuration of them all. What is an application without a database to store all of its precious data?

The data source configuration is what tells Wheels which database to use for all of its models. (This can be overridden on a per-model basis, but that will be covered later.) To set this up in Wheels, it's just as easy as the previous example:

CFScript
set(dataSourceName="yourDataSourceName");
set(dataSourceUserName="yourDataSourceUsername");
set(dataSourcePassword="yourDataSourcePassword");

Function Settings

OK, here it's where the fun begins! Wheels includes a lot of functions to make your life as a CFML developer easier. A lot of those functions have sensible default argument values to minimize the amount of code that you need to write. And yes, you guessed it, Wheels lets you override those default argument values application-wide.

Let's look at a little of example:

CFScript
set(functionName="findAll", perPage=20);

Debugging and Error Settings

You'll generally want to configure how Wheels handles errors and debugging information based on your environment. For example, you probably won't want to expose CFML errors in your production environment, whereas you would want to see those errors in your development environment.

For example, let's say that we want to enable debugging information in our "development" environment temporarily:

CFScript
// /app/config/development/settings.cfm
set(showDebugInformation=false);

Full Listing of Debugging and Error Settings

Name
Type
Default
Description

errorEmailServer

string

[empty string]

Server to use to send out error emails. When left blank, this defaults to settings in the ColdFusion Administrator (if set).

errorEmailAddress

string

[empty string]

Comma-delimited list of email address to send error notifications to. Only applies if sendEmailOnError is set to true.

errorEmailSubject

string

Error

Subject of email that gets sent to administrators on errors. Only applies if sendEmailOnError is set to true.

excludeFromErrorEmail

string

[empty string]

List of variables available in the scopes to exclude from the scope dumps included in error emails. Use this to keep sensitive information from being sent in plain text over email.

sendEmailOnError

boolean

Enabled in production environments that have a TLD like .com, .org, etc.

When set to true, Wheels will send an email to administrators whenever Wheels throws an error.

showDebugInformation

boolean

Enabled in development mode.

When set to true, Wheels will show debugging information in the footers of your pages.

showErrorInformation

boolean

Enabled in development, maintenance, and testing mode.

When set to false, Wheels will run and display code stored at app/events/onerror.cfm instead of revealing CFML errors.

Caching Settings

Wheels does a pretty good job at caching the framework and its output to speed up your application. But if personalization is key in your application, finer control over caching settings will become more important.

Let's say your application generates dynamic routes and you need it to check the routes on each request. This task will be as simple as this line of code:

CFScript
set(cacheRoutes=false);

Full Listing of Caching Settings

Name
Type
Default
Description

cacheActions

boolean

Enabled in maintenance, testing, and production

When set to true, Wheels will cache output generated by actions when specified (in a caches() call, for example).

cacheControllerConfig

boolean

Enabled in development, maintenance, testing, and production

When set to false, any changes you make to the config() function in the controller file will be picked up immediately.

cacheCullInterval

numeric

5

Number of minutes between each culling action. The reason the cache is not culled during each request is to keep performance as high as possible.

cacheCullPercentage

numeric

10

If you set this value to 10, then at most, 10% of expired items will be deleted from the cache.

cacheDatabaseSchema

boolean

Enabled in development, maintenance, testing, and production

When set to false, you can add a field to the database, and Wheels will pick that up right away.

cacheFileChecking

boolean

Enabled in development, maintenance, testing, and production

When set to true, Wheels will cache whether or not controller, helper, and layout files exist

cacheImages

boolean

Enabled in development, maintenance, testing, and production

When set to true, Wheels will cache general image information used in imageTag() like width and height.

cacheModelConfig

boolean

Enabled in development, maintenance, testing, and production

When set to false, any changes you make to the config() function in the model file will be picked up immediately.

cachePages

boolean

Enabled in maintenance, testing, and production

When set to true, Wheels will cache output generated by a view page when specified (in a renderView() call, for example).

cachePartials

boolean

Enabled in maintenance, testing, and production

When set to true, Wheels will cache output generated by partials when specified (in a includePartial() call for example).

cacheQueries

boolean

Enabled in maintenance, testing, and production

When set to true, Wheels will cache SQL queries when specified (in a findAll() call, for example).

clearQueryCacheOnReload

boolean

true

Set to true to clear any queries that Wheels has cached on application reload.

cacheRoutes

boolean

Enabled in development, maintenance, testing, and production

When set to true, Wheels will cache routes across all page views.

defaultCacheTime

numeric

60

Number of minutes an item should be cached when it has not been specifically set through one of the functions that perform the caching in Wheels (i.e., caches(), findAll(), includePartial(), etc.)

maximumItemsToCache

numeric

5000

Maximum number of items to store in Wheels's cache at one time. When the cache is full, items will be deleted automatically at a regular interval based on the other cache settings.

ORM Settings

The Wheels ORM provides many sensible conventions and defaults, but sometimes your data structure requires different column naming or behavior than what Wheels expects out of the box. Use these settings to change those naming conventions or behaviors across your entire application.

For example, if we wanted to prefix all of the database table names in our application with blog_ but didn't want to include that at the beginning of model names, we would do this:

CFScript
set(tableNamePrefix="blog_");

Now your post model will map to the blog_posts table, comment model will map to the blog_comments table, etc.

Full Listing of ORM Settings

Name
Type
Default
Description

afterFindCallbackLegacySupport

boolean

true

When this is set to false and you're implementing an afterFind() callback, you need to write the same logic for both the this scope (for objects) and arguments scope (for queries). Setting this to false makes both ways use the arguments scope so you don't need to duplicate logic. Note that the default is true for backwards compatibility.

automaticValidations

boolean

true

Set to false to stop Wheels from automatically running object validations based on column settings in your database.

setUpdatedAtOnCreate

boolean

true

Set to false to stop Wheels from populating the updatedAt timestamp with the createdAt timestamp's value.

softDeleteProperty

string

deletedAt

Name of database column that Wheels will look for in order to enforce soft deletes.

tableNamePrefix

string

[empty string]

String to prefix all database tables with so you don't need to define your model objects including it. Useful in environments that have table naming conventions like starting all table names with tbl

timeStampOnCreateProperty

string

createdAt

Name of database column that Wheels will look for in order to automatically store a "created at" time stamp when records are created.

timeStampOnUpdateProperty

string

updatedAt

Name of the database column that Wheels will look for in order to automatically store an "updated at" time stamp when records are updated.

transactionMode

string

commit

Use commit, rollback, or none to set default transaction handling for creates, updates and deletes.

useExpandedColumnAliases

boolean

false

When set to true, Wheels will always prepend children objects' names to columns included via findAll()'s include argument, even if there are no naming conflicts. For example, model("post").findAll(include="comment") in a fictitious blog application would yield these column names: id, title, authorId, body, createdAt, commentID, commentName, commentBody, commentCreatedAt, commentDeletedAt. When this setting is set to false, the returned column list would look like this: id, title, authorId, body, createdAt, commentID, name, commentBody, commentCreatedAt, deletedAt.

modelRequireConfig

boolean

false

Set to true to have Wheels throw an error when it can't find a config() method for a model. If you prefer to always use config() methods, this setting could save you some confusion when it appears that your configuration code isn't running due to misspelling "config" for example.

Plugin Settings

There are several settings that make plugin development more convenient. We recommend only changing these settings in development mode so there aren't any deployment issues in production, testing, and maintenancemodes. (At that point, your plugin should be properly packaged in a zip file.)

If you want to keep what's stored in a plugin's zip file from overwriting changes that you made in its expanded folder, set this in app/config/development/settings.cfm:

CFScript
set(overwritePlugins=false);
Name
Type
Default
Description

deletePluginDirectories

boolean

true

When set to true, Wheels will remove subdirectories within the plugins folder that do not contain corresponding plugin zip files. Set to false to add convenience to the process for developing your own plugins.

loadIncompatiblePlugins

boolean

true

Set to false to stop Wheels from loading plugins whose supported versions do not match your current version of Wheels.

overwritePlugins

boolean

true

When set to true, Wheels will overwrite plugin files based on their source zip files on application reload. Setting this to false makes plugin development easier because you don't need to keep rezipping your plugin files every time you make a change.

showIncompatiblePlugins

boolean

false

When set to true, an incompatibility warning will be displayed for plugins that do not specify the current Wheels version.

Media Settings

Full Listing of Asset Settings

Name
Type
Default
Description

assetQueryString

boolean

false in development mode, true in the other modes

Set to true to append a unique query string based on a time stamp to JavaScript, CSS, and image files included with the media view helpers. This helps force local browser caches to refresh when there is an update to your assets. This query string is updated when reloading your Wheels application. You can also hard code it by passing in a string.

assetPaths

struct

false

Pass false or a struct with up to 2 different keys to autopopulate the domains of your assets: http (required) and https. For example: {http="asset0.domain1.com,asset2.domain1.com,asset3.domain1.com", https="secure.domain1.com"}

Routing Settings

Wheels includes a powerful routing system. Parts of it are configurable with the following settings.

Full Listing of Miscellaneous Settings

Name
Type
Default
Description

loadDefaultRoutes

boolean

true

Set to false to disable Wheels's default route patterns for controller/action/key, etc.

obfuscateUrls

boolean

false

Set to true to obfuscate primary keys in URLs.

View Helper Settings

Wheels has a multitude of view helpers for building links, forms, form elements, and more. Use these settings to configure global defaults for their behavior.

Name
Type
Default
Description

booleanAttributes

any

allowfullscreen, async, autofocus, autoplay, checked, compact, controls, declare, default, defaultchecked, defaultmuted, defaultselected, defer, disabled, draggable, enabled, formnovalidate, hidden, indeterminate, inert, ismap, itemscope, loop, multiple, muted, nohref, noresize, noshade, novalidate, nowrap, open, pauseonexit, readonly, required, reversed, scoped, seamless, selected, sortable, spellcheck, translate, truespeed, typemustmatch, visible

A list of HTML attributes that should be allowed to be set as boolean values when added to HTML tags (e.g. disabled instead of disabled="disabled"). You can also pass in true(all attributes will be boolean) or false (no boolean attributes allowed, like in XHTML).

CSRF Protection Settings

Wheels includes built-in Cross-Site Request Forgery (CSRF) protection for form posts. Part of the CSRF protection involves storing an authenticity token in the session (default) or within an encrypted cookie. Most of the settings below are for when you've chosen to store the authenticity token within a cookie instead of the server's session store.

Name
Type
Default
Description

csrfStore

string

session

Which storage strategy to use for storing the CSRF authenticity token. Valid values are session or cookie. Choosing session requires no additional configuration. Choosing cookie for this requires additional configuration listed below.

csrfCookieEncryptionAlgorithm

string

AES

Encryption algorithm to use for encrypting the authenticity token cookie contents. This setting is ignored if you're using session storage. See your CF engine's documentation for the Encrypt()function for more information.

csrfCookieEncryptionSecretKey

string

Secret key used to encrypt the authenticity token cookie contents. This value must be configured to a string compatible with the csrfCookieEncryptionAlgorithmsetting if you're using cookie storage. This value is ignored if you're using session storage. See your CF engine's documentation for the Encrypt()function for more information.

csrfCookieEncryptionEncoding

string

Base64

Encoding to use to write the encrypted value to the cookie. This value is ignored if you're using session storage. See your CF engine's documentation for the Encrypt() function for more information.

csrfCookieName

string

_wheels_authenticity

The name of the cookie to be set to store CSRF token data. This value is ignored if you're using session storage.

csrfCookieDomain

string

Domain to set the cookie on. See your CF engine's documentation for cfcookie for more information.

csrfCookieEncodeValue

boolean

Whether or not to have CF encode the cookie. See your CF engine's documentation for cfcookie for more information.

csrfCookieHttpOnly

boolean

true

Whether or not they have CF set the cookie as HTTPOnly. See your CF engine's documentation for cfcookie for more information.

csrfCookiePath

string

/

Path to set the cookie on. See your CF engine's documentation for cfcookie for more information.

csrfCookiePreserveCase

boolean

Whether or not to preserve the case of the cookie's name. See your CF engine's documentation for cfcookie for more information.

csrfCookieSecure

boolean

Whether or not to only allow the cookie to be delivered over the HTTPS protocol. See your CF engine's documentation for cfcookie for more information.

CORS Protection Settings

Wheels includes built-in Cross-Origin Resource Sharing (CORS) which allows you to configure which cross-origin requests and methods are allowed. By default, this feature is turned off which will deny cross-origin requests at the browser level.

In this first version, the user can enable this feature, which will allow requests from all origins and all methods.

Name
Type
Default

allowCorsRequests

boolean

false

Miscellaneous Settings

Name
Type
Default
Description

disableEngineCheck

boolean

false

Set to true if you don't want Wheels to block you from using older CFML engines (such as ColdFusion 9, Railo etc).

enableMigratorComponent

boolean

true

Set to false to completely disable the migrator component which will prevent any Database migrations

enablePluginsComponent

boolean

true

Set to false to completely disable the plugins component which will prevent any plugin loading, and not load the entire plugins system

enablePublicComponent

boolean

true

Set to false to completely disable the public component which will disable the GUI even in development mode

Migrator Configuration Settings

Setting
Type
Default
Description

autoMigrateDatabase

Boolean

false

Automatically runs available migration on applicationstart.

migratorTableName

String

migratorversions

The name of the table that stores the versions migrated.

createMigratorTable

Boolean

true

Create the migratorversions database table.

writeMigratorSQLFiles

Boolean

false

Writes the executed SQL to a .sql file in the app/migrator/sql directory.

migratorObjectCase

String

lower

Specifies the case of created database object. Options are 'lower', 'upper' and 'none' (which uses the given value unmodified)

allowMigrationDown

Boolean

false (true in development mode)

Prevents 'down' migrations (rollbacks)

Conventions

With a convention-over-configuration framework like Wheels, it's important to know these conventions. This is your guide.

There is a specific set of standards that Wheels follows when you run it in its default state. This is to save you time. With conventions in place, you can get started coding without worrying about configuring every little detail.

But it is important for you to know these conventions, especially if you're running an operating system and/or DBMS configuration that's picky about things like case sensitivity.

URLs

Wheels uses a very flexible routing system to match your application's URLs to controllers, views, and parameters.

Within this routing system is a default route that handles many scenarios that you'll run across as a developer. The default route is mapped using the pattern [controller]/[action]/[key].

Consider this example URL:http://localhost/users/edit/12

http://localhost/users/edit/12

This maps to the Users controller, edit action, and a key of 12. For all intents and purposes, this will load a view for editing a user with a primary key value in the database of 12.

This URL pattern works up the chain and will also handle the following example URLs:

URL
Controller
Action
Key

users

edit

12

users

new

users

index

Naming Conventions for Controllers, Actions, and Views

Controllers, actions, and views are closely linked together by default. And how you name them will influence the URLs that Wheels will generate.

Controllers

First, a controller is a CFC file placed in the controllers folder. It should be named in PascalCase. For example, a site map controller would be stored at app/controllers/SiteMap.cfc.

Multi-word controllers will be delimited by hyphens in their calling URLs. For example, a URL of /site-map will reference the SiteMap controller.

Actions

Methods within the controllers, known as actions, should be named in camelCase.

Like with controllers, any time a capital letter is used in camelCase, a hyphen will be used as a word delimiter in the corresponding URL. For example, a URL of /site-map/search-engines will reference the searchEngines action in the SiteMap controller.

Views

By default, view files are named after the action names and are stored in folders that correspond to controller names. Both the folder names and view file names should be all lowercase, and there is no word delimiter.

In our /site-map/search-engines URL example, the corresponding view file would be stored at app/views/sitemap/searchengines.cfm.

Layouts

A special type of view file called a layout defines markup that should surround the views loaded by the application. The default layout is stored at app/views/layout.cfm and is automatically used by all views in the application.

Controller-level layouts can also be set automatically by creating a file called layout.cfm and storing it in the given controller's view folder. For example, to create a layout for the users controller, the file would be stored at app/views/users/layout.cfm.

When a controller-level layout is present, it overrides the default layout stored in the root app/views folder.

Naming Conventions for Models and Databases

By default, the names of Wheels models, model properties, database tables, and database fields all relate to each other. Wheels even sets a sensible default for the CFML data source used for database interactions.

Data Sources

By default, the datasource is set to wheels.dev in the app/config/settings.cfm file. You can change the value in the set(dataSourceName="wheels.dev") function to whatever you want the name of the datasource to be.

Plural Database Table Names, Singular Model Names

Wheels adopts a Rails-style naming conventions for database tables and model files. Think of a database table as a collection of model objects; therefore, it is named with a plural name. Think of a model object as a representation of a single record from the database table; therefore, it is named with a singular word.

For example, a user model represents a record from the users database table. Wheels also recognizes plural patterns like binary/binaries, mouse/mice, child/children, etc.

Like controller files, models are also CFCs and are named in PascalCase. They are stored in the app/models folder. So the user model would be stored at app/models/User.cfc.

Everything in the Database is Lowercase

In your database, both table names and column names should be lowercase. The customersegments table could have fields called title, regionid, and incomelevel, for example.

Because of CFML's case-insensitive nature, we recommend that you refer to model names and corresponding properties in camelCase. This makes for easier readability in your application code.

In the customersegments example above, you could refer to the properties in your CFML as title, regionId, and incomeLevel to stick to CFML's Java-style roots. (Built-in CFML functions are often written in camelCase and PascalCase, after all.)

Configuration and Defaults

There are many default values and settings that you can tweak in Wheels when you need to. Some of them are conventions and others are just configurations available for you to change. You can even change argument defaults for built-in Wheels functions to keep your code DRYer.

Request Handling

How Wheels handles an incoming request

Wheels is quite simple when it comes to figuring out how incoming requests map to code in your application. Let's look at a URL for an e-commerce website and dissect it a little.

But before we do that, a quick introduction to URLs in Wheels is in order.

A Wheels URL

URLs in Wheels generally look something like this:

http://localhost/index.cfm/shop/products

It's also possible that the URLs will look like this in your application:

http://localhost/index.cfm?controller=shop&action=products

This happens when your web server does not support the cgi.path_info variable.

Regardless of your specific setup, Wheels will try to figure out how to handle the URLs. If Wheels fails to do this properly (i.e. you know that your web server supports cgi.path_info, but Wheels insists on creating the URLs with the query string format), you can override it by setting URLRewriting in app/config/settings.cfm to either On, Partial, or Off. The line of code should look something like this:

set(URLRewriting="Partial");

"Partial URL Rewriting" is what we call that first URL up there with the /index.cfm/ format.

In the example URLs used above, shop is the name of the controller to call, and products is the name of the action to call on that controller.

Model-View-Controller Explained

Unless you're familiar with the Model-View-Controller pattern, you're probably wondering what controllers and actions are.

Put very simply, a controller takes an incoming request and, based on the parameters in the URL, decides what (if any) data to get from the model (which in most cases means your database), and decides which view (which in most cases means a CFML file producing HTML output) to display to the user.

An action is an entire process of executing code in the controller, including a view file and rendering the result to the browser. As you will see in the example below, an action usually maps directly to one specific function with the same name in the controller file.

Creating URLs

For the remainder of this chapter, we'll type out the URLs in this shorter and prettier way.

A Wheels Page

Let's look a little closer at what happens when Wheels receives this example incoming request.

First, it will create an instance of the shop controller (app/controllers/Shop.cfc) and call the function inside it named products.

Let's show how the code for the products function could look to make it more clear what goes on:

Shop.cfc
component extends="Controller" {

    function products() {
        renderView(controller="shop", action="products");
    }

}

Wheels Conventions

Because Wheels favors convention over configuration, we can remove a lot of the code in the example above, and it will still work because Wheels will just guess what your intention is. Let's have a quick look at exactly what code can be removed and why.

Therefore, the code above can be changed to:

Shop.cfc
component extends="Controller" {

    function products() {
        renderView();
    }

}

… and it will still work just fine.

That leaves you with this code:

Shop.cfc
component extends="Controller" {

    function products() { 
    }

}

That looks rather silly, a products function with no code whatsoever. What do you think will happen if you just remove that entire function, leaving you with this code?

Shop.cfc
component extends="Controller" { 

}

…If you guessed that Wheels will just assume you don't need any code for the products action and just want the view rendered directly, then you are correct.

In fact, if you have a completely blank controller like the one above, you can delete it from the file system altogether!

This is quite useful when you're just adding simple pages to a website and you don't need the controller and model to be involved at all. For example, you can create a file named about.cfm in the app/views/home folder and access it at http://localhost/home/about without having to create a specific controller and/or action for it, assuming you're still using wildcard routing.

This also highlights the fact that Wheels is a very easy framework to get started in because you can basically program just as you normally would by creating simple pages like this and then gradually "Wheelsifying" your code as you learn the framework.

The params Struct

Besides making sure the correct code is executed, Wheels also does something else to simplify request handling for you. It combines the url and form scopes into one. This is something that most CFML frameworks do as well. In Wheels, it is done in the params struct.

The params struct is available to you in the controller and view files but not in the model files. (It's considered a best practice to not mix your request handling with your business logic.) Besides the form and url scope variables, the params struct also contains the current controller and action name for easy reference.

If the same variable exists in both the url and form scopes, the value in the form scope will take precedence.

To make this concept easier to grasp, imagine a login form on your website that submits to http://localhost/account/login?sendTo=dashboard with the variables username and password present in the form. Your params struct would look like this:

Name
Value

params.controller

account

params.action

login

params.sendTo

dashboard

params.username

joe

params.password

1234

Now instead of accessing the variables as url.sendTo, form.username, etc., you can just use the params struct for all of them instead.

JSON as part of the request body

If you're constructing a JSON API, you're inevitably going to come across how to deal with "incoming" json packets to your routing endpoints. Instead of having to test each request with whether it's valid json etc, Wheels will automatically map the JSON body in a request to the params struct, as long as it has application/json as it's mime type.

So in the same way that the url and form scopes are merged, so a valid json body would be. The exception to the rule is when a javascript array is the root element, where it's then added to params._json to follow Rails conventions. (For obvious reasons, we can't merge an array into a struct!)

NOTE

The mapping of a json array to params._json was introduced in Wheels 2.1

Routing

Contributing to Wheels

Here are the rules of the road for contributing code to the project. Let's follow this process so that we don't duplicate effort and so the code base is easy to maintain over the long haul.

Repository

Anyone may fork the cfwheels repository, make changes, and submit a pull request.

Core Team Has Write Access

To make sure that we don't have too many chefs in the kitchen, we will be limiting direct commit access to a core team of developers.

At this time, the core team consists of these developers:

This does not restrict you from being able to contribute. See "Process for Implementing Code Changes" below for instructions on volunteering. With enough dedication, you can earn a seat on the core team as well!

Process for Implementing Code Changes

Here's the process that we'd like for you to follow. This process is in place mainly to encourage everyone to communicate openly. This gives us the opportunity to have a great peer-review process, which will result in quality. Who doesn't like quality?

  1. A member of the core team will review your submission and leave feedback if necessary.

  2. Once the core team member is satisfied with the scope of the issue, they will indicate so in a comment to the issue. This is your green light to start working. Get to coding, grasshopper!

  3. When you have implemented your enhancement or change, use your Git repository to create a pull request with your changes.

    • You should annotate your commits with the issue number as #55 if the code issue is 55

  4. A core team member will review it and post any necessary feedback in the issue tracker.

  5. Once everything is resolved, a core team member will merge your commit into the Git repository.

Developing with Docker

Code Style

Additionally, we recommend that any applications written using the Wheels framework follow the same style. This is optional, of course, but still strongly recommended.

Supported CFML Engines

All code for Wheels should be written for use with both Adobe ColdFusion 2018 upwards, and Lucee 5 upwards.

Naming Conventions

To stay true to our ColdFusion and Java roots, all names must be camelCase. In some cases, such as internal CFML functions, the first letter should be capitalized as well. Refer to these examples.

Code Element
Examples
Description

CFC Names

MyCfc.cfc, BlogEntry.cfc

CapitalizedCamelCase

Variable Names

myVariable, storyId

camelCase

UDF Names

myFunction()

camelCase

Built-in CF Variables

result.recordCount, cfhttp.fileContent

camelCase

Built-in CF Functions

IsNumeric(), Trim()

CapitalizedCamelCase

Scoped Variables

application.myVariable, session.userId

lowercase.camelCase

CGI Variables

cgi.remote_addr, cgi.server_name

cgi.lowercase_underscored_name

CFC Conventions

Local Variables

Since moving to Wheels 2.x, the old loc scope has now been deprecated and you should use the function local scope.

Code Example

// This is just for illustration. It's obviously a silly function.
function returnArrayLengthInWords(required array someArray) {
  local.rv = "Unknown";
  if (ArrayLen(arguments.someArray) > 50) {
    local.rv = "Pretty Long!";
  }
  if (ArrayLen(arguments.someArray) > 100) {
    local.rv = "Very Long!";
  }
  return local.rv;
}

CFC Methods

All CFC methods should be made public. If a method is meant for internal use only and shouldn't be included in the API, then prefix it with a dollar sign $. An example would be $query().

Documenting your Code

One of a developer’s biggest time sinks is coming up with and writing accurate and engaging documentation. Even if it’s an internal project with.. well, just you… it’s still incredibly valuable to have: coming back to projects a few months or even years later can often lead to a bit of head scratching and screams of “why?”, especially with your own code.

Whilst we’re not promising that Wheels will write all your documentation for you, we have put some tools in place to hopefully make the process a little less painful. With a small amount of adjustment in how you document your core functions, documentation doesn’t necessarily have to be such a time consuming process.

In the Wheels core, we've adopted javadoc-style syntax (with a few twists of our own) to automatically create our main documentation, and there's nothing stopping you adding to this core set of documentation for your own app. It just needs a bit of markup.

Browse the Core API

The first thing to notice is the new ‘[Docs]’ link in the debug section in the footer: Following that link leads you to the main internal documentation.

The three-column layout is designed to allow for quick filtering by section or function name. On the right are the main Wheels core categories, such as Controller and Model functions. These categories are further divided into subcategories, such as Flash and Pagination functions. Clicking on a link in the right column will filter the list in the left and middle columns to show all matching functions, including child functions of that category.

Filtering by function name is made simple by a “filter-as-you-type” search field in the left column, making it very quick to find the right function.

The middle column contains the main function definition, including tags, parameters and code samples.

How is it generated?

Each function in the core is now appropriately marked up with javaDoc style comments. This, combined with getMetaData() allows us to parse the markup into something useful.

Example Core Function

Example Core Function
/**
 * Removes all HTML tags from a string.
 *
 * [section: View Helpers]
 * [category: Sanitization Functions]
 *
 * @html The HTML to remove tag markup from.
 */
public string function stripTags(required string html) {
    local.rv = REReplaceNoCase(arguments.html, "<\ *[a-z].*?>", "", "all");
    local.rv = REReplaceNoCase(local.rv, "<\ */\ *[a-z].*?>", "", "all");
    return local.rv;
}

The [section] and [category] tags categorise the function as appropriate, and the @html part describes the function’s parameter. The additional parameter data, such as whether it’s required, type and any defaults are automatically parsed too. This results in a display like:

Documenting your own functions

Any function which is available to Controller.cfc or Model.cfc is automatically included; if there’s no javaDoc comment, then they’ll appear in uncategorized. But of course, there’s nothing stopping you creating your own [section] and [category] tags, which will then automatically appear on the left hand column for filtering: you’re not restricted to what we’ve used in the core.

As an example, if you wanted to document all your filters, you might want to have a [section: Application] tag, with [category: filters]. This way, your application documentation grows as you create it.

Something as simple as a sublime text snippet for a new function which includes the basic javaDoc skeleton can get you in the habit pretty quickly!

Plugins too!

We also introspect plugins for the same markup. We encourage plugin authors to adjust their plugin code to include [section: Plugins] at the very least.

Exports & Re-use

You can export the docs via JSON just by changing the URL string: i.e ?controller=wheels&action=wheels&view=docs&type=core&format=json

You could also include the functions used to create the docs and create your own version (perhaps useful for a CMS or other application where you have internal documentation for authenticated users). Whilst this isn’t officially supported (the main functions may be subject to change!) it is technically possible. The best example is the thing itself – see the main output if you’re interested. Please note that the user interface isn’t available in production mode (for obvious reasons we hope!), so if you wanted to expose this data to an end user, you would probably need to “roll your own” with this approach.

Updating

Note that the CFC introspection doesn’t automatically happen on every request, so you will need to ?reload=true to see changes to your code. Additionally, Adobe ColdFusion is more aggressive in caching CFC metadata, so depending on your settings, you may not see changes until a server restart.

Code samples

Whilst the core has additional code samples which can be loaded from text files, there’s no support for your application functions to take advantage of this yet.

Roadmap

Whilst this API/function explorer is a great first step, you’ll notice your controller and model specific functions aren’t included (only those shared amongst controllers, or in the /app/global/functions.cfm file. This is because we’re only looking at the main Model.cfc and Controller.cfc and what it can access.

In Wheels 2.1, we’ll look at adding a full Controller and Model metadata explorer using the same techniques, and map functions like show() to their respective routes too.

Directory Structure

Finding your way around a Wheels application.

After downloading and unzipping Wheels, here's the directory structure that you will see:

app/ build/ db/ docker/ guides/ public/ tests/ vendor/ .cfformat.json .editorconfig .env CFConfig.json box.json compose.yml server.json

Quick Summary

Your configuration settings will be done in the app/config directory.

Your application code will end up in four of the folders, namely app/controllers, app/events, app/models, and app/views.

Static media files should be placed in the public/files, public/images, public/javascripts and public/stylesheets folders.

Place anything that need to be executed outside of the framework in the public/miscellaneous folder. The framework does not get involved when executing .cfm files in this folder. (The empty Application.cfc takes care of that.) Also, no URL rewriting will be performed in this folder, so it's a good fit for placing CFCs that need to be accessed remotely via <cfajaxproxy> and Flash AMF binding, for example.

Place Wheels plugins in the app/plugins folder.

And the last directory? That's the framework itself. It exists in the vendor/wheels directory. Please go in there and have a look around. If you find anything you can improve or new features that you want to add, let us know!

Detailed Overview

Let's go through all the files and directories now, starting with the ones you'll spend most of your time in: the code directories.

app/controllers

This is where you create your controllers. You'll see a file in here already: Controller.cfc. You can place functions inside this Controller.cfc to have those functions shared between all the controllers you create(This works because all your controllers will extend Controller.).

app/models

This is where you create your model files (or classes if you prefer that term). Each model file you create should map to one table in the database.

The setup in this directory is similar to the one for controllers, to share methods you can place them in the existing Model.cfc file.

app/views

This is where you prepare the views for your users. As you work on your website, you will create one view directory for each controller.

app/events

If you want code executed when ColdFusion triggers an event, you can place it here (rather than directly in Application.cfc).

app/config

Make all your configuration changes here. You can set the environment, routes, and other settings here. You can also override settings by making changes in the individual settings files that you see in the subdirectories.

public/files

Any files that you intend to deliver to the user using the sendFile() function should be placed here. Even if you don't use that function to deliver files, this folder can still serve as file storage if you like.

app/global

For application-wide globally accessible functions

public/images

This is a good place to put your images. It's not required to have them here, but all Wheels functions that involve images will, by convention, assume they are stored here.

public/javascripts

This is a good place to put your JavaScript files.

public/stylesheets

This is a good place to put your CSS files.

tests/Testbox

This is where unit tests for your application should go

public/miscellaneous

Use this folder for code that you need to run completely outside of the framework. (There is an empty Application.cfc file in here, which will prevent Wheels from taking part in the execution.)

This is most useful if you're using Flash to connect directly to a CFC via AMF binding or if you're using <cfajaxproxy>in your views to bind directly to a CFC as well.

app/plugins

Place any plugins you have downloaded and want installed here.

app/migrator *

Database Migration CFC files and generated SQL files (This directory will only visible once you start using the migrator)

vendor/wheels

This is the framework itself. When a new version of Wheels is released it is often enough to just drop it in here (unless there has been changes to the general folder structure).

.htaccess

This file is used by Apache, and you specifically need it for URL rewriting to work properly. If you're not using Apache, then you can safely delete it. No longer included by default in 2.x

public/urlrewrite.xml

web.config

URL rewriting for version 7 of IIS. If you're not using IIS, then you can safely delete it. No longer included by default in 2.x

Application.cfc and index.cfm

These are needed for the framework to run. No changes should be done to these files.

You can add more directories if you want to, of course. Just remember to include a blank Application.cfc in those directories. Otherwise, Wheels will try to get itself involved with those requests as well.

\

wheels plugins - commands

These are the commands in the wheels plugins namespace. Currently this section is pretty sparse, make suggestions of what you may want to see in this section.

wheels plugins list

This command will make a call to ForgeBox.io and bring back a list of available Wheels Plugins listed on ForgeBox.

Testing Your Application

With Wheels, writing automated tests for your application is part of the development lifecycle itself, and running the tests is as simple as clicking a link.

Why Test?

At some point, your code is going to break. Upgrades, feature enhancements, and bug fixes are all part of the development lifecycle. Quite often with deadlines, you don't have the time to test the functionality of your entire application with every change you make.

The problem is that today's fix could be tomorrow's bug. What if there were an automated way of checking if that change you're making is going to break something? That's where writing tests for your application can be invaluable.

The Test Framework

Testbox is a simple yet powerful tool for testing your application. It contains not only a testing framework, runner, assertions and expectations library but also ships with MockBox, A Mocking & Stubbing Framework. It also supports xUnit style of testing and MXUnit compatibilities.

Conventions

In order to run tests against your application, all tests must reside in the tests/Testbox directory off the root of your Wheels application, or within a subdirectory thereof.

When you run the tests for your application, Testbox recursively scans your application's tests/Testbox/specs directory for valid tests. Whilst you have freedom to organize your subdirectories, tests and supporting files any way you see fit, we would recommend using the directory structure below as a guide:

What are these directories for?

The "functions" directory might contain test packages that cover model methods, global or view helper functions.

The "requests" directory might contain test packages that cover controller actions and the output that they generate (views).

Any components that will contain tests must extend the testbox.system.BaseSpec component:

If the testing framework sees that a component does not extend testbox.system.BaseSpec, that component will give error.

you can write a test method with the following syntax:

You also have to write your test methods inside the describe method like the following:

Using the describe method lets you bundle your tests inside a file. This way, you can have mutiple bundles inside a single file. You can name your tests and your bundles anything you want inside the "" but for convention's sake, you should start your bundles name with "Tests".

if you want any helper methods for your tests, you can write them outside all the describe methods in your file.

Do not var-scope any variables used in your tests. In order for the testing framework to access the variables within the tests that you're writing, all variables need to be within the component's variables scope. The easy way to do this is to just not var variables within your tests, and your CFML engine will automatically assign these variables into the variables scope of the component for you. You'll see this in the examples below.

Setup & Teardown

When writing a group of tests, it's common for there to be some duplicate code, global configuration, and/or cleanup needs that need to be run before or after each test. In order to keep things DRY (Don't Repeat Yourself), the TestBox offers 2 special methods that you can optionally use to handle such configuration.

beforeEach(() => {}): Used to initialize or override any variables or execute any code that needs to be run before each test.

afterEach(() => {}): Used to clean up any variables or execute any code that needs to be ran after each test.

Example:

Evaluation

expect().toBe(): This is the main method that you will be using when developing tests. You can use this to compare the result of an operation with a value that you expect the operation to return. Let's say you have the result of an operation stored in a variable result and you expect the result to be "run completed" then you can check if the result is indeed returning that value by doing expect(result).toBe("run completed").

An example test that checks that two values equal each other:

Either of the above will work. The toBe() method compares the value in expect() to the expected value, while toBeTrue() checks if the value in expect() is true. Another simple method is toBeFalse(), which checks if the value in expect() is false.

An example test that checks that the first value is less then the second value:

You get the idea since you've used these kinds of expressions a thousand times. You can compare structures, arrays, objects, you name it!

An example test that checks that a key exists in a structure:

When you want to test if an exception will be thrown, you can use the try{}catch{} to test for it. An example of raising the Wheels.TableNotFound error when you specify an invalid model name:

Debugging

debug(): Will display its output after the test result so you can examine an expression more closely.

expression (string) - a quoted expression to display label (string) - Attach a label to the expression

TIP

Overloaded arguments will be passed to the internal cfdump attributeCollection

Testing Your Models

The first part of your application that you are going to want to test against are your models because this is where all the business logic of your application lives. Suppose that we have the following model:

As you can see from the code above, our model has a beforeSave callback that runs whenever we save a user object. Let's get started writing some tests against this model to make sure that our callback works properly.

First, create a test component called /tests/Testbox/specs/models/TestUserModel.cfc, and in the beforeEach function, create an instance of the model that we can use in each test that we write. We will also create a structure containing some default properties for the model.

As you can see, we invoke our model by using the model() method just like you would normally do in your controllers.

The first thing we do is add a simple test to make sure that our custom model validation works.

Now that we have tests to make sure that our model validations work, it's time to make sure that the callback works as expected when a valid model is created.

Testing Your Controllers

The next part of our application that we need to test is our controller. Below is what a typical controller for our user model would contain for creating and displaying a list of users:

Notice the return in the create action in the redirectTo() method? The reason for this is quite simple, under the covers, when you call redirectTo(), Wheels is using cflocation. As we all know, there is no way to intercept or stop a cflocation from happening. This can cause quite a number of problems when testing out a controller because you would never be able to get back any information about the redirection.

To work around this, the Wheels test framework will "delay" the execution of a redirect until after the controller has finished processing. This allows Wheels to gather and present some information to you about what redirection will occur.

The drawback to this technique is that the controller will continue processing and as such we need to explicitly exit out of the controller action on our own, thus the reason why we use return.

Let's create a test package called /tests/Testbox/specs/controllers/TestUsersController.cfc to test that the create action works as expected:

Notice that a lot more goes into testing a controller than a model. The first step is setting up the params that will need to be passed to the controller. We then pass the 'params' to the processRequest() function which returns a structure containing a bunch of useful information.

We use this information to make sure that the controller redirected the visitor to the index action once the action was completed.

Note: processRequest() is only for use within the test framework.

Below are some examples of how a controller can be tested:

Testing Controller Variables

If you want to test a variable that's being set on a controller you can make use of the this scope. This way it's available from outside the controller, which makes it testable.

If you think that's too "ugly", you can instead make a public function on the controller that returns the value and then call that from your tests.

Testing Partials

You may at some point want to test a partial (usually called via includePartial()) outside of a request. You'll notice that if you just try and call includePartial() from within the test suite, it won't work. Thankfully there's a fairly easy technique you can use by calling a "fake" or "dummy" controller.

Testing Your Views

Next we will look at testing the view layer. Below is the code for new.cfm, which is the view file for the controller's new action:

Testing the view layer is very similar to testing controllers, we will setup a params structure to pass to the processRequest() function which will return (among other things) the generated view output.

Once we have this output, we can then search through it to make sure that whatever we wanted the view to display is presented to our visitor. In the test below, we are simply checking for the heading.

Testing Your Application Helpers

Next up is testing global helper functions. Below is a simple function that removes spaces from a string.

Remember to restart your application after adding a helper function to use it afterwards.

Testing these helpers is fairly straightforward. All we need to do is compare the function's return value against a value that we expect, using the assert() function.

Testing Your View Helpers

Testing your view helpers are very similar to testing application helpers except we need to explicitly include the helpers in the beforeEach function so our view functions are available to the test framework.

Below is a simple function that returns a string wrapped in h1 tags.

And in our view test package:

Testing Plugins

Testing plugins requires slightly different approaches depending on the mixin attribute defined in the plugin's main component.

Below is a simple plugin called timeAgo that extends Wheels' timeAgoInWords view helper by appending "ago" to the function's return value. Take note of the mixin="controller" argument as this will play a part in how we test the plugin.

In order to test our plugin, we'll need to do a little setup. Our plugin's tests will reside in a directory within our plugin package named tests. We'll also need a directory to keep test assets, in this case a dummy controller that we will need to instantiate in our test's beforeEach() function.

The /app/plugins/timeago/tests/assets/controllers/Dummy.cfc controller contains the bare minimum for a controller.

Firstly, in our /app/plugins/timeago/tests/TestTimeAgo.cfc we'll need to copy the application scope so that we can change some of Wheels' internal paths. Fear not, we'll reinstate any changes after the tests have finished executing using the AfterEach() function. so that if you're running your tests on your local development machine, your application will continue to function as expected after you're done testing.

Once the setup is done, we simply execute the plugin functions and check using expect() function that the return values are what we expect.

If your plugin is uses mixin="model", you will need to create and instantiate a dummy model component.

Testing Plugins with RocketUnit (Deprecated)

Testing plugins requires slightly different approaches depending on the mixin attribute defined in the plugin's main component.

Below is a simple plugin called timeAgo that extends Wheels' timeAgoInWords view helper by appending "ago" to the function's return value. Take note of the mixin="controller" argument as this will play a part in how we test the plugin.

In order to test our plugin, we'll need to do a little setup. Our plugin's tests will reside in a directory within our plugin package named tests. We'll also need a directory to keep test assets, in this case a dummy controller that we will need to instantiate in our test's setup() function.

The /plugins/timeago/tests/assets/controllers/Dummy.cfc controller contains the bare minimum for a controller.

Firstly, in our /plugins/timeago/tests/TestTimeAgo.cfc we'll need to copy the application scope so that we can change some of Wheels' internal paths. Fear not, we'll reinstate any changes after the tests have finished executing using the teardown function. so that if you're running your tests on your local development machine, your application will continue to function as expected after you're done testing.

Once the setup is done, we simply execute the plugin functions and assert that the return values are what we expect.

If your plugin is uses mixin="model", you will need to create and instantiate a dummy model component.

Running Your Tests

You can run your tests by clicking on the Testbox button in your navbar. It will open a dropdown menu which will have two options. App Tests and Core Tests. You can run either the framework's tests by clicking on the Core Tests or you can run your own tests that you have written for your application by clicking on App Tests. Clicking on either of them will open another dropdown menu which will have 4 options: HTML, JSON, TXT and JUnit. These are the formats in which you can get the result of your tests. After choosing your desired output format, click on that option. A new tab will open and you will get your test results after they have run.

The test URL will look something like this: /testbox

Running an individual package: /testbox?testBundles=controllers

Running a single test: /testbox?testBundles=controllers&testSpecs=testCaseOne

These URLs are useful should you want an external system to run your tests.

Test Results Format

Wheels can return your test results in either HTML, JSON, TXT or JUnit formats, simply by using the format url parameter. Eg: format=junit

Additional Techniques

Whilst best practice recommends that tests should be kept as simple and readable as possible, sometimes moving commonly used code into test suite helpers can greatly improve the simplicity of your tests.

Some examples may include, serializing complex values for use in assert() or grouping multiple assertions together. Whatever your requirements, there are a number of ways to use test helpers.

  1. Put your helper functions in your /tests/Testbox/Test.cfc. These will be available to any package that extends this component. Be mindful of functions you put in here, as it's easy to create naming collisions.

  2. If you've arranged your tests into subdirectories, you can create a helpers.cfm file in any given directory and simply include it in the package.

  3. Put package-specific helper functions in the same package as the tests that use it. These will only be available to the tests in that package. To ensure that these test helpers are not run as tests, use a function name that doesn't start with "test_". Eg: $simplify()

  • Overloading application vars.. Wheels will revert the application scope after all tests have completed.

Caveat: The test suite request must complete without uncaught exceptions. If an uncaught exception occurs, the application scope may stay 'dirty', so it's recommended to reload the application by adding reload=true param to your url whilst developing your test packages.

Learn By Example: Wheels Core

Responding with Multiple Formats

Wheels controllers provide some powerful mechanisms for responding to requests for content in XML, JSON, and other formats. You can build an API with ease using these functions.

If you've ever needed to create an XML or JSON API for your Wheels application, you may have needed to go down the path of creating a separate controller or separate actions for the new format. This introduces the need to duplicate model calls or even break them out into a super long list of before filters. With this, your controllers can get pretty hairy pretty fast.

Using a few Wheels functions, you can easily respond to requests for HTML, XML, JSON, and PDF formats without adding unnecessary bloat to your controllers.

Requesting Different Formats

With Wheels Provides functionality in place, you can request different formats using the following methods:

  1. URL Variable

  2. URL Extension

  3. Request Header

Which formats you can request is determined by what you configure in the controller. See the section below on Responding to Different Formats in the &#xNAN;Controller for more details.

URL Variable

Wheels will accept a URL variable called format. If you wanted to request the XML version of an action, for example, your URL call would look something like this:

The same would go for JSON:

URL Extension

Perhaps a cleaner way is to request the format as a "file" extension. Here are the XML and JSON examples, respectively:

This works similarly to the URL variable approach mentioned above in that there will now be a key in the params struct set to the format requested. With the XML example, there will be a variable at params.format with a value of xml.

Request Header

If you are calling the Wheels application as a web service, you can also request a given format via the HTTP Accept header.

If you are consuming the service with another Wheels application, your <cfhttp> call would look something like this:

In this example, we are sending an Accept header with the value for the xml format.

  • html

  • xml

  • json

  • csv

  • pdf

  • xls

Responding to Different Formats in the Controller

Take a look at this example:

When Wheels handles this response, it will set the appropriate MIME type in the Content-Type HTTP header as well.

Providing the HTML Format

Automatic Generation of XML and JSON Formats

Best Practices for Providing JSON

Unfortunately there have been a lot of JSON related issues in CFML over the years. To avoid as many of these problems as possible we thought we'd outline some best practices for you to follow.

The reason for doing it this way is that it will preserve the case for the struct / JSON keys.

With that in place you can be sure that firstName will always be treated as a string (i.e. wrap in double quotes) and booksForSale as an integer (i.e. no decimal places) when producing the JSON output. Without this, your CFML engine might guess what the data type is, and it wouldn't always be correct unfortunately.

Providing Your Own Custom Responses

If you need to provide content for another type than xml or json, or if you need to customize what your Wheels application generates, you have that option.

In your controller's corresponding folder in app/views, all you need to do is implement a view file like so:

If you need to implement your own XML-based or JSON-based output, the presence of your new custom view file will override the automatic generation that Wheels normally performs.

Example: PDF Generation

If you need to provide a PDF version of the product catalog, the view file at app/views/products/index.pdf.cfm may look something like this:

HTML

Rendering Content

Showing content to the user.

A big part of a controller's task is to respond to the user. In Wheels you can respond to the user in three different ways:

  • Displaying content

  • Redirecting to another URL

  • Sending a file

Rendering a Page

Rendering a Partial

Rendering Nothing at All

Rendering Text

Rendering to a Variable

Normally when you call any of the rendering functions, the result is stored inside an internal Wheels variable. This value is then output to the browser at the end of the request.

Caching the Response

Using a Layout

Using the Flash

Using the Flash to pass data from one request to the next.

The Flash is actually a very simple concept. And no, it has nothing to do with Adobe's Flash Player.

The Flash is just a struct in the session or cookie scope with some added functionality. It is cleared at the end of the next page that the user views. This means that it's a good fit for storing messages or variables temporarily from one request to the next.

By the way, the name "Flash" comes from Ruby on Rails, like so many other cool things in Wheels.

An Example of Using the Flash

The code below is commonly used in Wheels applications to store a message about an error in the Flash and then redirect to another URL, which then displays the message in its view page.

The following example shows how code dealing with the Flash can look in an action that handles a form submission.

Here's an example of how we then display the Flash message we just set in the view page for the edit action. Please note that this is done on the next request since we performed a redirect after setting the Flash.

The key chosen above is success, but it could have been anything that we wanted. Just like with a normal struct, the naming of the keys is your job.

As an example, you may choose to use one key for storing messages after the user made an error, called error, and another one for storing messages after a successful user operation, called success.

Shortcut for Setting the Flash and Redirecting

Therefore, you (the developer) must intend for it to be stored in the Flash, so Wheels goes ahead and calls flashInsert(success="The user was updated successfully.") for you behind the scenes.

Prepend with flash for Argument Names that Collide with redirectTos

So what if you want to redirect to the edit action and set a key in the Flash named action as well? Simply prepend the key with flash to tell Wheels to avoid the argument naming collision.

CFScript

We don't recommend naming the keys in your Flash action, but these naming collisions can potentially happen when you want to redirect to a route that takes custom arguments, so remember this workaround.

More Flashy Functions

Flash Storage Options

Earlier, we mentioned that the data for the Flash is stored in either the cookie or the session scope. You can find out where Wheels stores the Flash data in your application by outputting get("flashStorage"). If you have session management enabled in your application, Wheels will default to storing the Flash in the session scope. Otherwise, it will store it in a cookie on the user's computer.

Note: Before you set Wheels to use the session scope, you need to make sure that session management is enabled. To enable it, all you need to do is add this.SessionManagement = true to the app/config/app.cfm file.

Changing Flash Storage Dynamically During a Request

From Wheels 3.0, you now have the ability to dynamically change the flash storage during the lifecycle of a controller or request or the complete application using the setFlashStorage() function.

This can be helpful if you need to switch between using session-based or cookie-based flash storage at runtime depending on the context of the request.

By default, it changes the storage for the current controller only. However, if you want to change it globally for all subsequent requests (until changed again), you can pass a second argument setGlobally=true:

The storage argument accepts only session or cookie. If any other value is passed, Wheels will simply ignore the change (no error will be thrown). Always make sure to pass a valid storage type to avoid unexpected behavior.

Note: Be careful when changing flash storage dynamically during requests, as it will change the entire application's flash scope depending on the settings and this may result in logical errors depending on your flash settings, especially if your application relies heavily on a consistent flash storage mechanism across multiple requests or clusters.

Choosing a Storage Method

So what storage option should you choose? Well, to be honest, it doesn't really matter that much, so we recommend you just go with the default setting. If you're a control freak and always want to use the optimal setting, here are some considerations.

  • Although the Flash data is deserialized before stored in a cookie (making it possible to store complex values), you need to remember that a cookie is not the best place to store data that requires a lot of space.

  • If you run multiple ColdFusion servers in a clustered environment and use session-based Flash storage, users might experience a loss of their Flash variables as their request gets passed to other servers.

  • Using cookies is, generally speaking, less secure than using the session scope. Users could open their cookie file up and manually change its value. Sessions are stored on the server, out of users' reach.

Appending to, rather than replacing the flash

From Wheels 2.1, you can now change the default flash behavior to append to an existing key, rather than directly replacing it. To turn on this behavior, add set(flashAppend=true) to you /app/config/settings.cfm file.

An example of where this might be useful:

Which, when output via flashMessages() would render:

With set(flashAppend=true), you can also directly pass in an array of strings like so:

Disabling the Flash Cookie

In 2.2 upwards:

There are certain circumstances where you might not be using the flash: for example, if you have wheels setup as a stateless API (perhaps in a cluster behind a load balancer) where you're using tokens vs sessions. In this circumstance, you've probably turned off session management, which only leaves cookie as a viable route. But if set to cookie, you always get Set-Cookie being returned, which is unnecessary.

In wheels 2.2 you can now set(flashStorage = "none") which will prevent the creation of the cookie, and also not try and write to a session.

Sending Email

Use Wheels to simplify the task of setting up automated emails in your application.

Getting this to work in Wheels can be broken down in 3 steps. We'll walk you through them.

Establishing Mail Server and Account Information

This setting should be done in the app/config/settings.cfm file and can look something like this:

Alternatively, most modern CFML engines allow setting SMTP information directly within the application configuration. So you can actually add this in /app/config/app.cfm: here's an example configuration:

Create an Email Template

Consider this example scenario:

Multiple templates may be stored within this directory should there be a need.

The content of the template is simple: simply output the content and any expected variables.

Here's an example for myemailtemplate.cfm, which will contain HTML content.

Sending the Email

Consider the following example:

Here we are sending an email by including the myemailtemplate template and passing values for recipientNameand startDate to it.

Note that the template argument should be the path to the view's folder name and template file name without the extension. If the template is in the current controller, then you don't need to specify a folder path to the template file. In that case, just be sure to store the template file in the folder with the rest of the views for that controller.

Did you notice that we did not have to specify the type attribute of cfmail? Wheels is smart enough to figure out that you want to send as HTML since you have tags in the email body. (You can override this behavior if necessary though by passing in the type argument.)

Multipart Emails

The intelligence doesn't end there though. You can have Wheels send a multipart email by passing in a list of templates to the templates argument (notice the plural), and Wheels will automatically figure out which one is text and which one is HTML.

Attaching Files

You can attach files to your emails as well by using the file argument (or files argument if you want multiple attachments). Simply pass in the name of a file that exists in the public/files folder (or a subfolder of it) of your application.

Alternatively you can pass in mail parameters directly if you require more control (such as sending a dynamically generated PDF which isn't written to disk):

Using Email Layouts

A layout should be used just as the name implies: for layout and stylistic aspects of the email body. Based on the example given above, let's assume that the same email content needs to be sent twice.

  • Message is sent to a new member with a stylized header and footer.

  • A copy of message is sent to an admin at your company with a generic header and footer.

Best practice is that variables (such as recipientName and startDate, in the example above) be placed as outputs in the template file.

Multipart Email Layouts

Wheels also lets you set up layouts for the HTML and plain text parts in a multipart email.

For both the templates and layouts arguments (again, notice the plurals), we provide a list of view files to use. Wheels will figure out which of the templates and layouts are the HTML versions and separate out the MIME parts for you automatically.

Go Send Some Emails

Now you're all set to send emails the Wheels way. Just don't be a spammer, please!

Redirecting Users

Use redirection to keep your application user friendly.

When a user submits a form, you do not want to show any content on the page that handles the form submission! Why? Because if you do, and the user hits refresh in their browser, the form handling code could be triggered again, possibly causing duplicate entries in your database, multiple emails being sent, etc.

Remember to Redirect

Three Ways to Redirect

Let's look at the three ways you can redirect in Wheels.

1. Redirecting to Another Action

2. Redirection Using Routes

3. Redirecting to the Referring URL

Handling an Invalid Referrer

The referring URL is retrieved from the cgi.http_referer value. If this value is blank or comes from a different domain than the current one, Wheels will redirect the visitor to the root of your website instead.

Appending Params

Sometimes it's useful to be able to send the visitor back to the same URL they came from but with extra parameters added to it. You can do this by using the params argument. Note that Wheels will append to the URL and not replace it in this case.

The addToken and statusCode Arguments

You can also set the type of redirect to something other than the default 302 redirect, by passing in statusCode=3xx. For example, 301 indicates a permanent redirect.

Sending Files

Use Wheels to send files to your users securely and with better control of the user experience.

Sending files?! Is that really a necessary feature of Wheels? Can't I just place the file on my web server and link to it? You are correct, there is absolutely no need to use Wheels to send files. Your web server will do a fine job of sending out files to your users.

Sending Files with the sendFile() Function

The convention in Wheels is to place all files you want users to be able to download in the public/files folder.

Assuming you've placed a file named wheels_tutorial_20081028_J657D6HX.pdf in that folder, here is a quick example of how you can deliver that file to the user. Let's start with creating a link to the action that will handle the sending of the file first.

Here's the sendTutorial action:

That's one ugly file name though, eh? Let's present it to the user in a nicer way by suggesting a different name to the browser:

Much better! :)

Here's an example:

Example

Sending Files via ram://

You can even send files which are stored in ram:// - this is particularly useful when you're dynamically creating files (such as PDF reports) which don't need to be written to the file system.

Securing Access to Files

However, there is a security flaw here. Can you figure out what it is?

You may have guessed that the files folder is placed in your web root, so anyone can download files from it by typing http://www.domain.com/files/wheels_tutorial_20081028_J657D6HX.pdf in their browser. Although users would need to guess the file names to be able to access the files, we would still need something more robust as far as security goes.

There are two solutions to this.

The easiest one is to just lock down access to the folder using your web server. Wheels won't be affected by it since it gets the file from the file system.

This assumes you've moved the folder two levels up in your file system and into a folder named "tutorials".

Don't Open Any Holes with URL Parameters

By far the quickest way to get started with Wheels is via . CommandBox brings a whole host of command line capabilities to the CFML developer. It allows you to write scripts that can be executed at the command line written entirely in CFML. It allows you to start a CFML server from any directory on your machine and wire up the code in that directory as the web root of the server. What's more is, those servers can be either Lucee servers or Adobe ColdFusion servers. You can even specify what version of each server to launch. Lastly, CommandBox is a package manager for CFML. That means you can take some CFML code and package it up into a module, host it on ForgeBox.io, and make it available to other CFML developers. In fact we make extensive use of these capabilities to distribute Wheels plugins and templates. More on that later.

The first step is to get downloaded and running. CommandBox is available for Windows, Mac & Linux, and can be installed manually or using one of the respective package managers for each OS. You can use on Windows, on MacOS, or Yum/Apt on Linux depending on your flavor of Linux. Please follow the instructions on how to install CommandBox on your particular operating system. At the end of the installation process you want to make sure the box command is part of your system path so you can call the command from any directory on your system.

You can set what you want to use as your reload password or accept the default. Please make sure to change this before you go into production. Ideally this should be kept out of your source repository by using something like the (Wheels DotEnvSettings Plugin)[].

You can also set values based on what environment you have set. For example, you can have different values for your settings depending on whether you're in development mode or production mode. See the chapter on for more details.

To change a Wheels application default, you generally use the function. With it, you can perform all sorts of tweaks to the framework's default behaviors.

Use the function to access the value of a Wheels application setting. Just pass it the name of the setting.

For more information, read the chapter about .

That little line of code will make all calls to the method in Wheels return a maximum number of 20 record per page (if pagination is enabled for that call). How great is that? You don't need to set the perPage value for every single call to if you have a different requirement than the Wheels default of 10 records.

For more information, refer to the chapter about .

For more information, refer to the chapter on .

See the chapter on for more information.

Configure how Wheels handles linking to assets through view helpers like , , and .

See the chapter about for more information.

See the chapters about and for more information about how this all works together.

new

Note that the above conventions are for GET requests and only apply when you have a wildcard() call in app/config/routes.cfm (which is the default). See for instructions on overriding this behavior and how to deal with PUT, POST etc.

See for instructions on overriding this behavior.

See for instructions on overriding this behavior.

For information on overriding this behavior, refer to documentation for the function and read the chapter.

For information on overriding the layout file to be loaded by an action, see the chapter on and documentation for the function.

Refer to the chapter for instructions on overriding data source information.

For instructions on overriding database naming conventions, refer to documentation for the function and the chapter on .

For information on overriding column and property names, refer to documentation for the function and the chapter.

For more details on what you can configure, read the chapter.

Mapping an incoming URL to code is only one side of the equation. You will also need a way to create these URLs. This is done through a variety of different functions like (for creating links), (for creating forms), and (for redirecting users), to name a few.

Internally, all of these functions use the same code to create the URL, namely the function. The function accepts a controller and an action argument, which are what you will use most of the time. It has a lot of other arguments and does some neat stuff (like defaulting to the current controller when you don't specifically pass one in). So check out the documentation for the function for all the details.

By the way, by using URL rewriting in Apache or IIS, you can completely get rid of the index.cfm part of the URL so that http://localhost/index.cfm/shop/products becomes http://localhost/shop/products. You can read more about this in the chapter.

The only thing this does is specify the view page to render using the function.

The function is available to you because the shop controller extends the main Wheels Controllercomponent. Don't forget to include that extends attribute in your cfcomponent call as you build your controllers!

So, how does work? Well, it accepts the arguments controller and action (among others, such as route), and, based on these, it will try to include a view file. In our case, the view file is stored at app/views/shop/products.cfm.

You can read the chapter about for more information about the function.

It's important to note that the function does not cause any controller actions or functions to be executed. It just specifies what view files to get content from. Keep this in mind going forward because it's a common assumption that it does. (Especially when you want to include the view page for another action, it's easy to jump to the incorrect conclusion that the code for that action would also get executed.)

The first thing Wheels assumes is that if you call without arguments, you want to include the view page for the current controller and action.

Does Wheels assume anything else? Sure it does. You can actually remove the entire call because Wheels will assume that you always want to call a view page when the processing in the controller is done. Wheels will call it for you behind the scenes.

This concept becomes even more useful once we start getting into creating forms specifically meant for accessing object properties. But let's save the details of all that for the chapter.

For more advanced URL-to-code mappings, you are encourage to use a concept called routing. It allows for you to fully customize every URL in your application, including which HTTP verb can be used. You can read more about this in the chapter called .

The official Git repository for Wheels is located at our .

Open an issue in the , outlining the changes or additions that you would like to make.

Need help or running across any issues while coding? Start a .

If needed, open an issue to have the additions and changes in your revision documented in the . You may claim the issue if you'd like to do this, but it's entirely your choice.

To easily develop and test Wheels locally on multiple CFML engines using Docker, check out the

All framework code should use the guidelines at . This will make things more readable and will keep everyone on the same page. If you're working on code and notice any violations of the official style, feel free to correct it!

If you use Tomcat and Tuckey, or CommandBox , you'll need this file. Otherwise, you can safely delete it.

For testing your application in Wheels, we have added a third party tool in the framework which doesn't come preinstalled but you can install it by running box install in the Commandbox from inside your application.

The Wheels core uses this test framework for its unit test suite and contains a wealth of useful examples. They can all be found in the of the Wheels git repo.

Here is a list of values that you can grab from with Wheels out of the box.

You can use to set more types to the appropriate MIME type for reference. For example, we could set a Microsoft Word MIME type in app/config/settings.cfm like so:

The fastest way to get going with creating your new API and formats is to call from within your controller's config() method.

By calling the function in config(), you are instructing the Wheels controller to be ready to provide content in a number of formats. Possible choices to add to the list are html (which runs by default), xml, json, csv, pdf, and xls.

This is coupled with a call to in the following actions. In the example above, we are setting a query result of products and passing it to . By passing our data to this function, Wheels gives us the ability to respond to requests for different formats, and it even gives us the option to just let Wheels handle the generation of certain formats automatically.

You can also use the call in an individual controller action to define which formats the action will respond with. This can be used to define behavior in individual actions or to override the controller's config().

Responding to requests for the HTML version is the same as you're already used to with . will accept the same arguments as , and you create just a view template in the views folder like normal.

If the requested format is xml or json, the function will automatically transform the data that you provide it. If you're fine with what the function produces, then you're done!

First of all, always return data as an array of structs. This is done by using the returnAs argument (on for example), like this:

Secondly, make use of Wheels ability to return the JSON values in a specified type. This is done in the function, like this:

Type
Example

You can only respond once per request. If you do not explicitly call any of the response functions ( , etc) then Wheels will assume that you want to show the view for the current controller and action and do it for you.

This chapter covers the first method listed above—displaying content. The chapters about and cover the other two response methods.

This is the most common way of responding to the user. It's done with the function, but most often you probably won't call it yourself and instead let Wheels do it for you.

Sometimes you will want to call it though and specify to show a view page for a controller/action other than the current one. One common technique for handling a form submission, for example, is to show the view page for the controller/action that contains the form (as opposed to the one that just handles the form submission and redirects the user afterwards). When doing this, it's very important to keep in mind that will not run the code for the controller's action—all it does is process the view page for it.

You can also call explicitly if you wish to cache the response or use a different layout than the default one.

If the controller and action arguments do not give you enough flexibility, you can use the template argument that is available for .

Refer to the chapter for more details about rendering content. More specifically, that chapter describes where to place those files and what goes in them.

This is done with the function. It's most often used with AJAX requests that are meant to update only parts of a page.

Sometimes you don't need to return anything at all to the browser. Perhaps you've made an AJAX request that does not require a response or executed a scheduled task that no end user sees the results of. In these cases you can use the function to tell Wheels to just render an empty page to the browser.

This is done with the function. It just returns the text you specify. In reality it is rarely used but could be useful as a response to AJAX requests sometimes.

Sometimes you may want to do some additional processing on the rendering result before outputting it though. This is where the returnAs argument comes in handy. It's available on both and . Setting returnAs to string will return the result to you instead of placing it in the internal Wheels variable.

Two of the functions listed above are capable of caching the content for you; and . Just pass in cache=true (to use the default cache time set in app/config/settings.cfm) or cache=x where x is the number of minutes you want to cache the content for. Keep in mind that this caching respects the global setting set for it in your configuration files so normally no pages will be cached when in Design or Development mode.

We cover caching in greater detail in the chapter.

The function accepts an argument named layout. Using this you can wrap your content with common header/footer style code. This is such an important concept though so we'll cover all the details of it in the chapter called .

As you can see above, you use the function with a named argument when you want to store data in the Flash and the function when you want to display the data in a view.

The more you work with Wheels and the Flash, the more that you're going to find that you keep repeating that / combo all the time. Wheels has a solution for that within the function itself:

That piece of code does exactly the same thing as the example shown previously in this chapter. The Wheels function sees the success argument coming in and knows that it's not part of its own declared arguments.

Besides and that are used to read from/insert to the Flash, there are a few other functions worth mentioning.

is used to count how many key/value pairs there are in the Flash.

and do exactly the same as their counterparts in the struct world, StructClear and StructDelete—they clear the entire Flash and delete a specific key/value from it, respectively.

is used to check if a specific key exists. So it would make sense to make use of that function in the code listed above to avoid outputting an empty <p> tag on requests where the Flash is empty. ( will return an empty string when the specified key does not exist.)

Check out the section in the API listing of all the functions that deal with the Flash.

Wholesale Flash Handling with

Throw the function into the mix, and you might find yourself writing code across your Wheels projects that looks something like this:

All of that above code can be replaced with a single call to the function:

Whenever any value is inserted into the Flash, will display it similarly to the complex example above, with class attributes set similarly (errorMessage for the error key and successMessage for the success key).

You can also use 's key/keys argument to limit its reach to a list of given keys. Let's say that we only want our layout to show messages for the alert key but not for the error or success keys (or any other for that matter). We would write our call like so:

Just keep in mind that this approach isn't as flexible, so if you need to customize the markup of the messages beyond 's capabilities, you should revert back to using , , and other related functions manually.

You can override this setting in the same way that you override other Wheels settings by running the function like this:

Sending emails in Wheels is done from your controller files with the function. It's basically a wrapper around cfmail, but it has some smart functionality and a more Wheels-like approach in general.

We recommend using Wheels ability to set global defaults for so that you don't have to specify more arguments than necessary in your controller files. Because it's likely that you will use the same mail server across your application, this makes it worthwhile to set a global default for it.

By specifying these values here, these arguments can be omitted from all function calls, thus providing cleaner, less cluttered code.

But you are not limited to setting only these 3 variables. In fact, you can set a global default for any optional argument to and since it accepts the same arguments that cfmail does. That's quite a few.

An email template is required for to work and forms the basis for the mail message content. Think of an email template as the content of your email.

Templates may be stored anywhere within the /app/views/ folder, but we recommend a structured, logical approach. If different controllers utilize and each require a unique template, place each email template within the app/views/controllername folder structure.

Controller:
Email Template:

As we've said before, accepts all attribute of CFML's cfmail tag as arguments. But it also accepts any variables that you need to pass to the email template itself.

The logic for which template file to include follows the same logic as the template argument to .

Like the template argument, the logic for which file to include follows the same logic as the template argument to .

Much like the layouts outlined in the chapter, you can also create layouts for your emails.

In this case, the two calls to would be nearly identical, with the exception of the layout argument.

If we set up generic email layouts at app/views/plainemaillayout.cfm and app/views/htmlemaillayout.cfm, we would call like so:

To avoid the above problem, it is recommended to always redirect the user after submitting a form. In Wheels this is done with the function. It is basically a wrapper around the cflocation tag in CFML.

Being that is a Wheels function, it can accept the route, controller, action, and key arguments so that you can easily redirect to other actions in your application.

You can redirect the user to another action in your application simply by passing in the controller, action, and key arguments. You can also pass in any other arguments that are accepted by the function, like host, params, etc. (The function is what Wheels uses internally to produce the URL to redirect to.)

If you have configured any routes in app/config/routes.cfm, you can use them when redirecting as well. Just pass in the route's name to the route argument together with any additional arguments needed for the route in question. You can read more about routing in the chapter.

It's very common that all you want to do when a user submits a form is send them back to where they came from. (Think of a user posting a comment on a blog post and then being redirected back to view the post with their new comment visible as well.) For this, we have the back argument. Simply pass in back=true to , and the user will be redirected back to the page they came from.

If you want to specify exactly where to send the visitor when the referring domain is blank/foreign, you can pass in the normal arguments like route, controller, action, etc. These will be used only when Wheels can't redirect to the referrer because it's invalid.

The function uses <cflocation> under the hood; if you need to pass client variable information automatically in the URL for client management purposes, simply set addToken=true.

However, if you want a little more control over the way the user's browser handles the download or be able to secure access to your files then you might find the function useful.

Now let's send the file to the user in the sendTutorial controller action. Just like the rendering functions in Wheels, the function should be placed as the very last thing you do in your action code.

In this case, that's the only thing we are doing, but perhaps you want to track how many times the file is being downloaded, for example. In that case, the tracking code needs to be placed before the function.

Also, remember that the function replaces any rendering. You cannot send a file and render a page. (This will be quite obvious once you try this code because you'll see that the browser will give you a dialog box, and you won't actually leave the page that you're viewing at the time.)

By default, the function will try and force a download dialog box to be shown to the user. The purpose of this is to make it easy for the user to save the file to their computer. If you want the file to be shown inside the browser instead (when possible as decided by the browser in question), you can set the disposition argument to inline.

You can also specify what HTTP content type to use when delivering the file by using the type argument. Please refer to the API for the function for complete details.

Perhaps the main reason to use the function is that it gives you an easy way to secure access to your files. Maybe the tutorial file used in the above example is something the user paid for, and you only want for that user to be able to download it (and no one else). To accomplish this, you can just add some code to authenticate the user right before the call in your action.

If that is not an option, the other option is simply to move the files folder out of the web root, thus making it inaccessible. If you move the folder, you'll need to accommodate for this in your code by changing your calls to specify the path as well, like this:

A final note of warning: Be careful to not allow just any parameters from the URL to get passed through to the because then a user would be able to download any file from your server by playing around with the URL. Be wary of how you're using the params struct in this context!

CommandBox
CommandBox
Chocolatey
Homebrew
https://www.forgebox.io/view/wheels-dotenvsettings
Switching Environments
set()
get()
URL Rewriting
findAll()
findAll()
findAll()
Switching Environments
Caching
Installing and Using Plugins
imageTag()
styleSheetLinkTag()
javaScriptIncludeTag()
Date, Media, and Text Helpers
Using Routes
Obfuscating URLs
Routing
Routing
Routing
renderView()
Pages
Layouts
renderView
Configuration and Defaults
table()
Object Relational Mapping
property()
Object Relational Mapping
Configuration and Defaults
linkTo()
startFormTag()
redirectTo()
URLFor()
URLFor()
URLFor()
URL Rewriting
renderView()
renderView()
renderView()
Rendering Content
renderView()
renderView()
renderView()
renderView()
Form Helpers and Showing Errors
Routing
GitHub repository
Peter Amiri
Zain Ul Abideen
issue tracker
Discussion
CHANGELOG
Docker Instructions
https://github.com/cfwheels/cfwheels/wiki/Code-Style-Guide
URL Rewriting
Environment settings
URL rewriting settings
Data source settings
Function settings
Debugging and error settings
Caching settings
ORM settings
Plugin settings
Media settings
Routing settings
View helper settings
CSRF protection settings
Miscellaneous Settings
Migrator settings
wheels plugins list
❯ wheels plugins list
================ Wheels Plugins From ForgeBox ======================
Contacting ForgeBox, please wait...

 Wheels Example Plugin    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-plugin-example"
Wheels Example Plugin


 Shortcodes    ( Tom King )
Type: Wheels Plugins
Slug: "shortcodes"
Shortcodes Plugin for Wheels


 Wheels iCal4j    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-ical4j"
Wheels 2.x Plugin Date Repeats Methods via iCal4J Java Lib


 Wheels JS Disable    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-js-disable"
JS Disable - Wheels Plugin


 Wheels bCrypt    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-bcrypt"
Wheels 2.x plugin helper methods for the bCrypt Java Lib


 Wheels JS Confirm    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-js-confirm"
JS Confirm - Wheels Plugin


 Wheels JWT    ( Tom King )
Type: Wheels Plugins
Slug: "wheels-jwt"
Wheels plugin for encoding and decoding JSON Web Tokens (JWT)


 Wheels SAML    ( Tom Sucaet )
Type: Wheels Plugins
Slug: "wheels-saml"
Wheels plugin for SAML Single Sign-On


 Wheels Models Default Scope    ( Landon Fabbricino )
Type: Wheels Plugins
Slug: "defaultScope"
Wheels 2.1+ Add default scope to models for FindAll method


 Bens Json Serializer For Wheels    ( Brandon Shea )
Type: Wheels Plugins
Slug: "wheels-bens-json-serializer"
Swaps renderWith()'s use of serializeJson() with Ben Nadel's JsonSerializer


 Wheels DotEnvSettings Plugin    ( Peter Amiri )
Type: Wheels Plugins
Slug: "wheels-dotenvsettings"
DotEnvSettings Plugin for Wheels


 TitleTag Plugin    ( Chris Geirman )
Type: Wheels Plugins
Slug: "wheels-titletag-plugin"
DRY up your title tags. Allows you to define each page's title within it's view.


 Wheels HTMX Plugin    ( Peter Amiri )
Type: Wheels Plugins
Slug: "wheels-htmx-plugin"
HTMX Plugin for Wheels


 Wheels File Bin    ( Brandon Shea )
Type: Wheels Plugins
Slug: "wheels-filebin"
Wheels File Bin


 wheels ckEditor plugin    ( Reuben Brown )
Type: Wheels Plugins
Slug: "wheels-ckeditor-plugin"
Over-ride the textArea() function


 wheels Bootstrap Multiselect plugin    ( Reuben Brown )
Type: Wheels Plugins
Slug: "wheels-bootstrapmultiselect-plugin"
Creates a new function to allow for a multiselect box


 Datepicker    ( Adam Chapman )
Type: Wheels Plugins
Slug: "datepicker"
Datepicker Plugin for Wheels



  Found 17 records.
======================================================================
tests/
  Testbox/
    specs/
    ├── functions/
    ├── requests/
component extends="testbox.system.BaseSpec" {
    // your tests here
}
it("Result is True", () => {
  result = true
	expect(result).toBeTrue()
})
describe("Tests that return True", () => {
  it("Result is True", () => {
    result = true
    expect(result).toBeTrue()
  })
})
beforeEach(() => {
  _controller = controller(name="dummy")

  args = {
    fromTime=Now(),
    includeSeconds=true;
  }
})

it("works with seconds below 5 seconds", () => {
	number = 5 - 1
	args.toTime = DateAdd('s', number, args.fromTime)
	actual = _controller.distanceOfTimeInWords(argumentCollection = args)

	expect(actual).toBe("less than 5 seconds")
})

it("works with seconds below 10 seconds", () => {
	number = 10 - 1
	args.toTime = DateAdd('s', number, args.fromTime)
	actual = _controller.distanceOfTimeInWords(argumentCollection = args)

	expect(actual).toBe("less than 10 seconds")
})
it("actual equals expected", () => {
  actual = true
  expected = true
  expect(actual).toBe(expected)
})
it("actual equals expected", () => {
  actual = true
  expect(actual).toBeTrue()
})
it("one is less than five", () => {
  one = 1
  five = 5
  expect(one).toBeLT(five)
})
it("key exists in structure", () => {
  struct = {
    foo="bar"
  }
  key = "foo"
  expect(struct).toHaveKey(key)
})
it("Table not found", () => {
	try {
		model('thisHasNoTable')
		$assert.fail("Wheels.TableNotFound error did not occur.")
	} catch (any e) {
		type = e.Type
		expect(type).toBe("Wheels.TableNotFound")
	}
})
it("key exists in structure", () => {
  struct = {
    foo="bar"
  }
  key = "foo"

  // displaying the debug output
  debug("struct")

  // displaying the output of debug with a label
  debug("struct", "my struct")

  expect(struct).toHaveKey(key)
})
component extends="Model" {
  public void function config() {
    // validation
    validate("checkUsernameDoesNotStartWithNumber")
    // callbacks
    beforeSave("sanitizeEmail");
  }

  /**
   * Check the username does not start with a number
   */
  private void function checkUsernameDoesNotStartWithNumber() {
    if (IsNumeric(Left(this.username, 1))) {
        addError(
        property="username",
        message="Username cannot start with a number."
      );
    }
  }

  /**
   * trim and force email address to lowercase before saving
   */
  private void function sanitizeEmail() {
      this.email = Trim(LCase(this.email));
  }
}
beforeEach(() => {
  // create an instance of our model
  user = model("user")

  // a structure containing some default properties for the model
  properties = {
      firstName="Hugh",
      lastName="Dunnit",
      email="hugh@example.com",
      username="myusername",
      password="foobar",
      passwordConfirmation="foobar"
  }
})
it("user model should fail custom validation", () => {
  // set the properties of the model
  user.setProperties(properties)
  user.username = "2theBatmobile!"

  // run the validation
  user.valid()

  actual = user.allErrors()[1].message
  expected = "Username cannot start with a number."

  // check that the expected error message is generated
  expect(actual).toBe(expected)
})
it("sanitize email callback should return expected value", () => {
  // set the properties of the model
  user.setProperties(properties)
  user.email = " FOO@bar.COM "

  /*
    Save the model, but use transactions so we don't actually write to
    the database. this prevents us from having to have to reload a new
    copy of the database every time the test runs.
  */
  user.save(transaction="rollback")

  // make sure that email address was sanitized
  expect(user.email).toBe("foo@bar.com")
})
component extends="Controller" {

  // users/index
  public void function index() {
    users = model("user").findAll()
  }

  // users/new
  public void function new() {
    user = model("user").new()
  }

  // users/create
  public any function create() {
    user = model("user").new(params.user)

    // Verify that the user creates successfully
    if (user.save()) {
      flashInsert(success="The user was created successfully.")
      // notice something about this redirectTo?
      return redirectTo(action="index")
    }
    else {
      flashInsert(error="There was a problem creating the user.")
      renderView(action="new")
    }
  }
}
it("redirect and flash status", () => {
  // define the controller, action and user params we are testing
  local.params = {
    controller="users",
    action="create",
    user={
      firstName="Hugh",
      lastName="Dunnit",
      email="hugh@somedomain.com",
      username="myusername",
      password="foobar",
      passwordConfirmation="foobar"
    }
    }

  // process the create action of the controller
  result = processRequest(params=local.params, method="post", returnAs="struct")

  // make sure that the expected redirect happened
  expect(result.status).toBe(302)
	expect(result.flash.success).toBe('The user was created successfully.')
	expect(result.redirect).toBe('/users/show/1')

})
// checks that a failed user update returns a 302 http response, an error exists in the flash and will be redirected to the error page
it("status flash and redirect", () => {
  local.params = {
    controller = "users",
    action = "update"
  }
  result = processRequest(params=local.params, method="post", rollback=true, returnAs="struct")
  expect(result.status).toBe(302)
  expect(result.flash).toHaveKey(error)
  expect(result.redirect).toBe('/common/error')
})

// checks that expected results are returned. Notice the update transactions is rolled back
it("status database update email and flash", () => {
  local.params = {
    controller = "users",
    action = "update",
    key = 1,
    user = {
      name = "Hugh"
    }
  }
  transaction {

    result = processRequest(params=local.params, method="post", returnAs="struct")

    user = model("user").findByKey(1)
    transaction action="rollback"
  }
  expect(result.status).toBe(302)
  expect(user.name).toBe('Hugh')
  expect(result.emails[1].subject).toBe('User was updated')
  expect(result.flash.success).toBe('User was updated')
})

// checks that an api request returns the expected JSON response
it("Test Json API", () => {
  local.params = {
    controller = "countries",
    action = "index",
    format = "json",
    route = "countries"
  }
  result = DeserializeJSON(processRequest(local.params)).data
  expect(result).toHaveLength(196)
})

// checks that an API create method returns the expected result
it("Test Json API create", () => {
  local.params = {
    action = "create",
    controller = "users",
    data = {
      type = "users",
      attributes = {
        "first-name" = "Hugh",
        "last-name" = "Dunnit"
      }
    },
    format = "json",
    route = "users"
  }
  result = processRequest(params=local.params, returnAs="struct").status;
  expect(result.status).toBe(201)
})
this.employeeNumber = params.empNum;

// Then from your test...

local.controller = controller(...);
local.controller.processAction();
theValue = local.controller.employeeNumber;
component extends="testbox.system.BaseSpec" {
    beforeEach(() => {
      params = {controller="dummy", action="dummy"}
      _controller = controller("dummy", params)
    })

    it("Test my partial", () => {
      result = _controller.includePartial(partial="/foo/bar/")
      expect(result).toInclude('foobar')
    })
}
<cfoutput>

<h1>Create a New user</h1>

#flashMessages()#

#errorMessagesFor("user")#

#startFormTag(route="users")#
    #textField(objectName='user', property='username')#
    #passwordField(objectName='user', property='password')#
    #passwordField(objectName='user', property='passwordConfirmation')#
    #textField(objectName='user', property='firstName')#
    #textField(objectName='user', property='lastName')#
    <p>
      #submitTag()# or
      #linkTo(text="Return to the listing", route="users")#
    </p>
#endFormTag()#

</cfoutput>
it("users index contains heading", () => {

  local.params = {
    controller = "users",
    action = "index"
  }

  result = processRequest(params=local.params, returnAs="struct")

  expect(result.status).toBe(200)
  expect(result.body).toHave('<h1>Create a New user</h1>')
})
// app/global/functions.cfm

public string function stripSpaces(required string string) {
    return Replace(arguments.string, " ", "", "all");
}
it("stripSpaces should return expected result", () => {
    actual = stripSpaces(" foo   -   bar     ")
    expected = "foo-bar"
    expect(actual).toBe(expected)
})
// app/views/helpers.cfm

public string function heading(required string text, string class="foo") {
    return '<h1 class="#arguments.class#">#arguments.text#</h1>';
}
beforeEach(() => {
  // include our helper functions
  include "/app/views/helpers.cfm"
  text = "Why so serious?"
})

it("heading returns expected markup", () => {
  actual = heading(text=text)
  expected = '<h1 class="foo">#text#</h1>'
  expect(actual).toBe(expected)
})

it("heading with class returns expected markup", () => {
  actual = heading(text=text, class="bar")
  expected = '<h1 class="bar">#text#</h1>'
  expect(actual).toBe(expected)
})
component mixin="controller" {

    public any function init() {
        this.version = "2.0";
        return this;
    }

    /*
     * Append the term "ago" to the timeAgoInWords core function
     */
    public string function timeAgo() {
        return core.timeAgoInWords(argumentCollection=arguments) & " " & __timeAgoValueToAppend();
    }

    /*
     * Define the term to append to the main function
     */
    private string function __timeAgoValueToAppend() {
        return "ago";
    }
}
app/
├─ plugins/
   └─ timeago/
      └─ TimeAgo.cfc
      └─ index.cfm
      └─ tests/
          └─ TestTimeAgo.cfc
          └─ assets/
              └─ controllers/
                  └─ Dummy.cfc
component extends="wheels.Controller" {
}
component extends="testbox.system.BaseSpec" {

	function run() {
		describe("Tests that timeAgo", () => {

			beforeEach(() => {
				// save the original environment
				applicationScope = Duplicate(application)
				// a relative path to our plugin's assets folder where we will store any plugin specific components and files
				assetsPath = "app/plugins/timeAgo/tests/assets/"
				// override wheels' path with our plugin's assets directory
				application.wheels.controllerPath = assetsPath & "controllers"
				// clear any plugin default values that may have been set
				StructDelete(application.wheels.functions, "timeAgo")
				// we're always going to need a controller for these tests so we'll just create a dummy
				_params = {controller="foo", action="bar"}
				dummyController = controller("Dummy", _params)
			})

			afterEach(() => {
				// reinstate the original application environment
				application = applicationScope;
			})

			// testing main public function
			it("timeAgo returns expected value", () => {
				actual = dummyController.timeAgo(fromTime=Now(), toTime=DateAdd("h", -1, Now()))
				expected = "About 1 hour ago"
				expect(actual).toBe(expected)
			})

			// testing the 'private' function
			it("timeAgo value to append returns expected value", () => {
				actual = dummyController.__timeAgoValueToAppend()
				expected = "ago"
				expect(actual).toBe(expected)
			})

		})
	}
}
component mixin="controller" {

    public any function init() {
        this.version = "2.0";
        return this;
    }

    /*
     * Append the term "ago" to the timeAgoInWords core function
     */
    public string function timeAgo() {
        return core.timeAgoInWords(argumentCollection=arguments) & " " & __timeAgoValueToAppend();
    }

    /*
     * Define the term to append to the main function
     */
    private string function __timeAgoValueToAppend() {
        return "ago";
    }
}
plugins/
├─ timeago/
    └─ TimeAgo.cfc
    └─ index.cfm
    └─ tests/
        └─ TestTimeAgo.cfc
        └─ assets/
            └─ controllers/
                └─ Dummy.cfc
component extends="wheels.Controller" {
}
component extends="wheels.Test" {

    function setup() {
        // save the original environment
        applicationScope = Duplicate(application);
        // a relative path to our plugin's assets folder where we will store any plugin specific components and files
        assetsPath = "plugins/timeAgo/tests/assets/";
        // override wheels' path with our plugin's assets directory
        application.wheels.controllerPath = assetsPath & "controllers";
        // clear any plugin default values that may have been set
        StructDelete(application.wheels.functions, "timeAgo";
        // we're always going to need a controller for these tests so we'll just create a dummy
        _params = {controller="foo", action="bar"};
        dummyController = controller("Dummy", _params);
    }

    function teardown() {
        // reinstate the original application environment
        application = applicationScope;
    }

    // testing main public function
    function testTimeAgoReturnsExpectedValue() {
        actual = dummyController.timeAgo(fromTime=Now(), toTime=DateAdd("h", -1, Now()));
        expected = "About 1 hour ago";
        assert("actual eq expected");
    }

    // testing the 'private' function
    function testTimeAgoValueToAppendReturnsExpectedValue() {
        actual = dummyController.__timeAgoValueToAppend();
        expected = "ago";
        assert("actual eq expected");
    }
}
component extends="tests.Testbox.Test" {

  // 1. All functions in /tests/Testbox/Test.cfc will be available

  // 2. Include a file containing helpers
  include "helpers.cfm";

  // 3. This is only available to this package
  function $simplify(required string string) {
    local.rv = Replace(arguments.string, " ", "", "all");
    local.rv = Replace(local.rv, Chr(13), "", "all");
    local.rv = Replace(local.rv, Chr(10), "", "all");
    return local.rv;
  }

}
http://www.example.com/products?format=xml
http://www.example.com/products?format=json
http://www.example.com/products.xml
http://www.example.com/products.json
<cfhttp url="http://www.example.com/products">
    <cfhttpparam type="header" name="Accept" value="application/octet-stream">
</cfhttp>
addFormat(extension="doc", mimeType="application/msword");
component extends="Controller" {

  function config(){
    provides("html,json,xml");
  }

  function index(){
    products = model("product").findAll(order="title");
    renderWith(products);
  }
}
authors = model("author").findAll(returnAs="structs");
renderWith(data=authors, firstName="string", booksForSale="integer");

html

app/views/products/index.cfm

xml

app/views/products/index.xml.cfm

json

app/views/products/index.json.cfm

csv

app/views/products/index.csv.cfm

pdf

app/views/products/index.pdf.cfm

xls

app/views/products/index.xls.cfm

<cfdocument format="pdf">
    <h1>Products</h1>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Description</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            #includePartial(products)#
        </tbody>
    </table>
</cfdocument>
function update() {
    if ( user.update(params.user) ) {
        flashInsert(success="The user was updated successfully.");
        redirectTo(action="edit");
    } else {
        renderView(action="edit");
    }
}
<cfoutput>
<p class="success-message">
    #flash("success")#
</p>
</cfoutput>
redirectTo(action="edit", success="The user was updated successfully.");
redirectTo(action="edit", flashAction="The user was updated successfully.");
<cfoutput>

<cfif NOT flashIsEmpty()>
    <div id="flash-messages">
        <cfif flashKeyExists("error")>
            <p class="errorMessage">
                #flash("error")#
            </p>
        </cfif>
        <cfif flashKeyExists("success")>
            <p class="successMessage">
                #flash("success")#
            </p>
        </cfif>
    </div>
</cfif>

</cfoutput>
<cfoutput>
#flashMessages()#
</cfoutput>
<cfoutput>
#flashMessages(key="alert")#
</cfoutput>
// In `app/config/settings.cfm` or another `settings.cfm` file within the `app/config` subfolders
set(flashStorage="session");
// Inside your controller action
setFlashStorage("cookie");
// Change globally for the entire application
setFlashStorage("session", true);
// In your controller
if( thingOneHappened ){
  flashInsert(info="Thing One Happened");
}
if( thingTwoHappened ){
  flashInsert(info="Thing Two Happened");
}
<div class=""flash-messages"">
  <p class=""info-message"">Thing One Happened</p>
  <p class=""info-message"">Thing Two Happened</p>
</div>
msgs=[
   "Thing One Happened", "Thing Two Happened"
];
flashInsert(info=msgs);
Example
set(
    functionName="sendEmail",
    server="yourServer",
    username="yourUsername",
    password="yourPassword"
);
app/config/app.cfm
// Lucee:
this.tag.mail.server="smtp.mydomain.com";
this.tag.mail.username="mySMTPUsername";
this.tag.mail.password="mySMTPPassword";
this.tag.mail.port=587; // Optional port
this.tag.mail.usetls=true; // Optional TLS

// Adobe ColdFusion
this.smtpServersettings = {
    'server' : 'smtp.thedomain.com:2525',
    'username' : 'xxxxxxxx',
    'password' : 'xxxxxxx'
};

Membership

/app/views/membership/myemailtemplate.cfm

Example
<cfoutput>
<p>Hi #recipientName#,</p>
<p>
    We wanted to take a moment to thank you for joining our service
    and to confirm your start date.
</p>
<p>
    Our records indicate that your membership will begin on
    <strong>#DateFormat(startDate, "dddd, mmmm, d, yyyy")#</strong>.
</p>
</cfoutput>
Example
member = model("member").findByKey(newMember.id);
sendEmail(
    from="service@yourwebsite.com",
    to=member.email,
    template="myemailtemplate",
    subject="Thank You for Becoming a Member",
    recipientName=member.name,
    startDate=member.startDate
);
// Send public/files/termsAndConditions.pdf
sendEmail(
    to="tom@domain.com",
    from="tom@domain.com",
    template="/email/exampletemplate",
    subject="Test",
    files="termsAndConditions.pdf"
);
Mail Example
// Create PDF
cfdocument(name="PDFContent", format="pdf"){
  writeOutput("<h1>Cats are better than dogz!</h1>");
};

// Setup Mail Params
mailParams[1]["file"]= "Test.pdf";
mailParams[1]["type"]= "application/pdf";
mailParams[1]["content"]= PDFContent;

sendEmail(
    to="tom@domain.com",
    from="tom@domain.com",
    template="/email/exampletemplate",
    subject="Test",
    mailParams=mailParams
);
Example
// Get new member.
member = model("member").findByKey(params.key);

// Customer email with customized header/footer.
sendEmail(
    from="service@yourwebsite.com",
    template="myemailtemplate",
    layout="customer",
    to=member.email,
    subject="Thank You for Becoming a Member",
    recipientName=member.name,
    startDate=member.startDate
);

// Plain text message with "admin" layout.
sendEmail(
    from="service@yourwebsite.com",
    template="myemailtemplate",
    layout="admin",
    to="admin@domain.com",
    subject="Membership Verification: #member.name#",
    recipientName=member.name,
    startDate=member.startDate
);
Example
// Multipart customer email.
sendEmail(
    from="service@yourwebsite.com",
    templates="/myemailtemplateplain,/myemailtemplatehtml",
    layouts="customerPlain,customerHtml",
    to=member.email,
    subject="Thank You for Becoming a Member",
    recipientName=member.name,
    startDate=member.startDate
);
<p>
  #linkTo(text="Download Tutorial", action="sendTutorial")#
</p>
Example
function sendTutorial() {
    sendFile(file="wheels_tutorial_20081028_J657D6HX.pdf");
}
Example
function sendTutorial() {
    sendFile(file="wheels_tutorial_20081028_J657D6HX.pdf", name="Tutorial.pdf");
}
function sendTutorial() {
    sendFile(file="wheels_tutorial_20081028_J657D6HX.pdf", disposition="inline");
}
Example
// Create the PDF.
wheels = cfdocument(format='pdf') {
 writeOutput(Now());
}; 

// Write the file to RAM.
fileWrite("ram://wheels.pdf", wheels);

// Send it.
sendFile(file="ram://wheels.pdf");
Example
function sendTutorial() {
    sendFile(file="../../tutorials/wheels_tutorial_20081028_J657D6HX.pdf");
}

Apache

URL rewriting instructions for Apache

Instructions for Apache

First in your web.xml add the following servlet mapping above the default servlet mapping:

web.xml
 <!-- The mapping for rewriting -->
    <servlet-mapping>
        <servlet-name>CFMLServlet</servlet-name>
        <url-pattern>/rewrite.cfm/*</url-pattern>
    </servlet-mapping>

For this you can use the global web.xml that for instance can be found at: /opt/lucee/tomcat/conf/web.xml. Or, if you are on shared hosting, use a local web.xml that for instance can be found at: yourwebroot/WEB-INF/web.xml.

After that use the following .htaccess file and Apache will pick up and use them automatically on server start-up.

Most of the time this will work. There are some exceptions though...

If you have installed Apache yourself you may need to turn on the rewrite module and/or change the security settings before URL rewriting will work:

  • Check that the Apache rewrite_module has been loaded by ensuring there are no pound signs before the line that says LoadModule rewrite_module modules/mod_rewrite.so in the httpd.conf file.

  • Make sure that Apache has permission to load the rewrite rules from the .htaccess file. This is done by setting AllowOverride to All under the Directory section corresponding to the website you plan on using Wheels on (still inside the httpd.conf file).

If you have an older version of Apache and you're trying to run your Wheels site in a sub folder of an existing site you may need to hard code the name of this folder in your rewrite rules.

  • Change the last line of the .htaccess file to the following: RewriteRule ^(.*)$ /sub_folder_name_goes_here/rewrite.cfm/$1 [L]. Don't forget to change sub_folder_name_goes_here to the actual folder name first of course.

.htaccess
Options +FollowSymLinks
RewriteEngine On
RewriteCond %{REQUEST_URI} ^.*/index.cfm/(.*)$ [NC]
RewriteRule ^.*/index.cfm/(.*)$ ./rewrite.cfm/$1 [NS,L]
RewriteCond %{REQUEST_URI} !^.*/(flex2gateway|jrunscripts|cfide|cf_scripts|cfformgateway|cffileservlet|lucee|files|images|javascripts|miscellaneous|stylesheets|wheels/public/assets|robots.txt|favicon.ico|sitemap.xml|rewrite.cfm)($|/.*$) [NC]
RewriteRule ^(.*)$ ./rewrite.cfm/$1 [NS,L]

Note that it's often considered better practice to include this URL rewriting configuration at the <virtualhost> block level, but get it working with a .htaccess file first.

Nesting Controllers

With the new routing system in Wheels 2.x, there are lots of nice features which allow for better code organization. One of these is the ability to nest controllers into folders using the namespace() method in our mapper() call.

For example, we may have a whole "Admin" section, where for each endpoint, we need to check some permissions, and possibly load some default data. Let's say we have a Users controller which provides standard CRUD operations.

app/config/routes.cfm
.mapper()
  .namespace("admin")
    .resources("users")
  .end()
.end()

This will automatically look for the Users.cfc controller in app/controllers/admin/.

By default, all your controllers extend="Controller", but with a nested controller, we need to change this, as the main Controller.cfc isn't at the same folder level.

A Handy Mapping

We've added a new mapping in 3.x, called app; This mapping will correspond to the app folder, so in our Users.cfc we now have two options - extend the core Controller.cfc via the app mapping, or perhaps extend another component (possibly Admin.cfc) which extends the core Controller instead.

app/controllers/admin/Users.cfc
component extends="app.controllers.Controller" {

  function config(){
    super.config();
  }

}

In the above example, we're using the app mapping to "go to" the app folder, and then look for a folder called controllers, and within that, our main Controller.cfc.

Our super.config() call will then run the config() function in our base Controller.

We could of course have the following too (just for completeness sake):

File system
/app/
  /controllers/
    /admin/
      - Admin.cfc
      - Users.cfc
    /public/
      - etc.

And then add the app.controllers.Controller mapping to Admin.cfc, and the extends="Admin" in the Users.cfc.

Not just controllers...

Of course, we can extend this concept (ha!) to Models too. However, this is either limited to tableless models, or models where you implicitly specify the table() call. As Wheels will look for the tablename dependent on the model file location, it'll get confused if in a sub-directory.

app/models/auth/LDAP.cfc
component extends="app.models.Model"
{
    function config() {
        table(false);
    }
    function save(){
    }
}

It also potentially makes your model() calls more complex, as you need to specify the model name in dot notation:

Example nested model call
// Example for "LDAP.cfc" in "/models/auth"
myNewLDAPModel=model("auth.LDAP").new();

Nginx

URL Rewriting for Nginx web server.

Example Nginx configuration

# nginx configuration 
location ~ .*/(flex2gateway|jrunscripts|cfide|cf_scripts|cfformgateway|cffileservlet|railo-context|lucee|files|images|javascripts|miscellaneous|stylesheets|robots.txt|favicon.ico|sitemap.xml|rewrite.cfm)($|/.*$) { }

location / {
rewrite ^(.*)$ https://YOURDOMAIN.com/$1 redirect; 
rewrite ^/.*/index.cfm/(.*)$ /rewrite.cfm/$1 break; 
rewrite ^(.*)$ /rewrite.cfm/$1 break; 
}
http://localhost/users/edit/12
http://localhost/users/
http://localhost/users
TestBox
tests_testbox folder
mimeTypes()
addFormat()
provides()
provides()
renderwith()
renderwith()
onlyProvides()
Rendering Content
renderwith()
renderView()
renderwith()
findAll()
renderWith()
renderView()
sendFile()
Redirecting Users
Sending Files
renderView()
renderView()
renderView()
renderView()
Pages
renderPartial()
renderNothing()
renderText()
renderView()
renderPartial()
renderView()
renderPartial()
Caching
renderView()
Using Layouts
flashInsert()
flash()
flashInsert()
redirectTo()
redirectTo()
redirectTo()
flash()
flashInsert()
flashCount()
flashClear()
flashDelete()
flashKeyExists()
flash()
Controller > Flash Functions
flashMessages()
flashIsEmpty()
flashMessages()
flashMessages()
flashMessages()
flashMessages()
flashIsEmpty()
flash()
set()
sendEmail()
sendEmail()
sendEmail()
sendEmail()
sendEmail()
sendEmail()
sendEmail()
renderView()
renderView()
Layouts
sendEmail()
sendEmail()
redirectTo()
redirectTo()
URLFor()
URLFor()
Routing
redirectTo()
URLFor()
redirectTo()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()
sendFile()

Routing

The routing system in Wheels encourages a conventional RESTful and resourceful style of request handling.

The Wheels routing system inspects a request's HTTP verb and URL and decides which controller and action to run.

Consider the following request:

HTTP
GET /products/5

The routing system may match the request to a route like this, which tells Wheels to load the show action on the Products controller:

.get(name="product", pattern="products/[key]", to="products##show")

Configuring Routes

To configure routes, open the file at app/config/routes.cfm.

In many cases, if you need to know where to go in the code to work with existing functionality in an application, the routes.cfm file can be a handy map, telling you which controller and action to start looking in.

How Routes Work

The various route mapping methods that we'll introduce in this chapter basically set up a list of routes, matching URL paths to a controllers and actions within your application.

The terminology goes like this:

Name

Method

A HTTP request method must be defined: GET, POST, PATCH, or DELETE.

You typically want to require POST, PATCH, or DELETE when a given action changes the state of your application's data:

  • Creating record(s): POST

  • Updating record(s): PATCH

  • Deleting record(s): DELETE

You can permit listing and showing records behind a normal HTTP GET request method.

Pattern and Parameters

A pattern is a URL path, sometimes with parameters in [squareBrackets]. Parameter values get sent to the controller in the params struct.

You'll see patterns like these in routes:

Example Route Patterns
posts/[key]/[slug]
posts/[key]
posts

In this example, key and slug are parameters that must be present in the URL for the first route to match, and they are required when linking to the route. In the controller, these parameters will be available at params.key and params.slug, respectively.

When a request is made to Wheels, the router will look for the first route that matches the requested URL. As an example, this means that if key is present in the URL but not slug, then it's the second route above that will match.

Viewing a List of Routes

In the debugging footer, you'll see a Routes link:

[info, View Routes, Docs, Tests, Migrator, Plugins]

Clicking that will load a filterable list of routes drawn in the app/config/routes.cfm file, including name, method, pattern, controller, and action.

Resource Routing

Many parts of your application will likely be CRUD-based (create, read, update, delete) for specific types of records (users, products, categories). Resources allow you to define a conventional routing structure for this common use case.

Resources are important

You'll want to pay close attention to how resource-based routing works because this is considered an important convention in Wheels applications.

/app/config/routes.cfm
mapper()
    .resources("products")
.end();

This will set up the following routes, pointing to specific actions within the products controller:

Name
HTTP Verb
Path
Controller & Action
Description

products

GET

/products

products.index

Display a list of all products

product

GET

/products/[key]

products.show

Display a specific product

newProduct

GET

/products/new

products.new

Display a form for creating a new product

products

POST

/products

products.create

Create a new product record

editProduct

GET

/products/[key]/edit

products.edit

Display a form for editing an existing product

product

PATCH/PUT

/products/[key]

products.update

Update an existing product record

product

DELETE

/products/[key]

products.delete

Delete an existing product record

Because the router uses a combination of HTTP verb and path, we only need 4 different URL paths to connect to 7 different actions on the controller.

What's with the PUT?

There has been some confusion in the web community on whether requests to update data should happen along with a PUT or PATCH HTTP verb. It has been settled mostly that PATCH is the way to go for most situations. Wheels resources set up both PUT and PATCH to address this confusion, but you should probably prefer linking up PATCH when you are able.

Singular Resources

Wheels also provides a singular resource for routing that will not expose a primary key through the URL.

mapper()
    .resource("cart")
.end();

This is handy especially when you're manipulating records related directly to the user's session (e.g., a profile or a cart can be managed by the user without exposing the primary key of the underlying database records).

Name
HTTP Verb
Path
Controller & Action
Description

cart

GET

/cart

carts.show

Display the cart

newCart

GET

/cart/new

carts.new

Display a form for creating a new cart

cart

POST

/cart

carts.create

Create a new cart record

editCart

GET

/cart/edit

carts.edit

Display a form for editing the cart

cart

PATCH/PUT

/cart

carts.update

Update the cart record

cart

DELETE

/cart

carts.delete

Delete the cart

Note that even though the resource path is singular, the name of the controller is plural by convention.

Also, this example is slightly contrived because it doesn't make much sense to create a "new" cart as a user typically just has one and only one cart tied to their session.

Defining Individual URL Endpoints

As you've seen, defining a resource creates several routes for you automatically, and it is great for setting up groupings of routes for managing resources within your application.

As a refresher, these are the intended purpose for each HTTP verb:

HTTP Verb
Meaning

GET

Display a list or record

POST

Create a record

PATCH/PUT

Update a record or set of records

DELETE

Delete a record

Security Warning

We strongly recommend that you not allow any GET requests to modify resources in your database (i.e., creating, updating, or deleting records). Always require POST, PUT, PATCH, or DELETE verbs for those sorts of routing endpoints.

Consider a few examples:

app/config/routes.cfm
mapper()
    .patch(name="heartbeat", to="sessions##update")

    .patch(
        name="usersActivate",
        pattern="users/[userKey]/activations",
        to="activations##update"
    )

    .resources("users")

    .get(name="privacy", controller="pages", action="privacy")
    .get(name="dashboard", controller="dashboards", action="show")
.end();

Rather than creating a whole resource for simple one-off actions or pages, you can create individual endpoints for them.

Notice that you can use the to="controller##action" or use separate controller/action arguments. The toargument allows you to delineate controller and action within a single string using a # separator (which must be escaped as a double ## because of CFML's special usage of that symbol within string syntax).

In fact, you could mock a users resource using these methods like so (though obviously there is little practical reason for doing so):

app/config/routes.cfm
mapper()
    // The following is roughly equivalent to .resources("users")
    .get(name="newUser", pattern="users/new", to="users##new")
    .get(name="editUser", pattern="users/[key]/edit", to="users##edit")
    .get(name="user", pattern="users/[key]", to="users##show")
    .patch(name="user", pattern="users/[key]", to="users##update")
    .put(name="user", pattern="users/[key]", to="users##update")
    .delete(name="user", pattern="users/[key]", to="users##delete")
    .post(name="users", to="users##create")
    .get(name="users", to="users##index")
.end();
app/config/routes.cfm
mapper()
    // Only offer endpoints for cart show, update, and delete:
    // -  GET /cart
    // -  PATCH /cart
    // -  DELETE /cart
    .resource(name="cart", only="show,update,delete")

    // Offer all endpoints for wishlists, except for delete:
    // -  GET /wishlists
    // -  GET /wishlists/new
    // -  GET /wishlists/[key]
    // -  GET /wishlists/[key]/edit
    // -  POST /wishlists
    // -  PATCH /wishlists/[key]
    .resources(name="wishlists", except="delete")
.end();

Browser Support for PUT, PATCH, and DELETE

While web standards advocate for usage of these specific HTTP verbs for requests, web browsers don't do a particularly good job of supporting verbs other than GET or POST.

To get around this, the Wheels router recognizes the specialized verbs from browsers (PUT, PATCH, and DELETE) in this way:

  • Via a POST request with a

  • POST variable named _method specifying the specific HTTP verb (e.g., _method=delete)

Note that using Wheels to write a REST API doesn't typically have this constraint. You should confidently require API clients to use the specific verbs like PATCH and DELETE.

Namespaces

The Wheels router allows for namespaces: the ability to add a route to a "subfolder" in the URL as well as within the controllers folder of your application.

Let's say that we want to have an "admin" section of the application that is separate from other "public" sections. We'd want for all of the "admin" controllers to be within an admin subfolder both in the URL and our application.

mapper()
    .namespace("admin")
        .resources("products")
    .end()
.end();

In this example, we have an admin section that will allow the user to manage products. The URL would expose the products section at /admin/products, and the controller would be stored at app/controllers/admin/Products.cfc.

Packages

Let's say that you want to group a set of controllers together in a subfolder (aka package) in your application but don't want to affect a URL. You can do so using the package mapper method:

mapper()
    .package("public")
        .resources("articles")
        .resource("profile")
    .end()
.end();

With this setup, end users will see /articles and /profile in the URL, but the controllers will be located at app/controllers/public/Articles.cfc and controllers/public/Profiles.cfc, respectively.

Nested Resources

You'll often find yourself implementing a UI where you need to manipulate data scoped to a parent record. Creating nested resources allows you to reflect this nesting relationship in the URL.

Let's consider an example where we want to enable CRUD for a customer and its children appointment records.

In this situation, we'd perhaps want for our URL to look like this for editing a specific customer's appointment:

HTTP
GET /customers/489/appointments/1909/edit

To code up this nested resource, we'd write this code in app/config/routes.cfm:

mapper()
    .resources(name="customers", nested=true)
        .resources("appointments")
    .end()
.end();

That will create the following routes:

HTTP Verb
Path
Controller & Action
Description

newCustomerAppointment

GET

/customers/[customerKey]/appointments/new

appointments.new

Display a form for creating a new appointment for a specific customer

customerAppointment

GET

/customers/[customerKey]/appointments/[key]

appointments.show

Display an existing appointment for a specific customer

editCustomerAppointment

GET

/customers/[customerKey]/appointments/[key]/edit

appointments.edit

Display a form for editing an existing appointment for a specific customer

customerAppointment

PATCH/PUT

/customers/[customerKey]/appointments/[key]

appointments.update

Update an existing appointment record for a specific customer

customerAppointment

DELETE

/customers/[customerKey]/appointments/[key]

appointments.delete

Delete an existing appointment record for an specific customer

customerAppointments

GET

/customers/[customerKey]/appointments

appointments.index

List appointments for a specific customer

customerAppointments

POST

/customers/[customerKey]/appointments

appointments.create

Create an appointment record for a specific customer

newCustomer

GET

/customers/new

customers.new

Display a form for creating a customer

customer

GET

/customers/[key]

customers.show

Display an existing customer

editCustomer

GET

/customers/[key]/edit

customers.edit

Display a form for editing an existing customer

customer

PATCH/PUT

/customers/[key]

customers.update

Update an existing customer

customer

DELETE

/customers/[key]

customers.delete

Delete an existing customer

customers

GET

/customers

customers.index

Display a list of all customers

customers

POST

/customers

customers.create

Create a customer

Notice that the routes for the appointments resource contain a parameter named customerKey. The parent resource's ID will always be represented by its name appended with Key. The child will retain the standard key ID.

You can nest resources and routes as deep as you want, though we recommend considering making the nesting shallower after you get to a few levels deep.

Here's an example of how nesting can be used with different route mapping methods:

mapper()
    // /products/[key]
    .resources(name="products", nested=true)
        // /products/[productKey]/promote
        .patch(name="promote", to="promotions##create")
        // /products/[productKey]/expire
        .delete(name="expire", to="expirations##create")

        // A 2nd-level resource
        // /products/[productKey]/variations/[key]
        .resources(name="variations", nested=true)
            // A 3rd-level resource
            // /products/[productKey]/variations/[variationKey]/primary
            .resource("primary")
        .end()
    .end()
.end();

Wildcard Routes

Wheels 1.x had a default routing pattern: [controller]/[action]/[key]. The convention for URLs was as follows:

HTTP
GET /news/show/5

With this convention, the URL above told Wheels to invoke the show action in the news controller. It also passed a parameter called key to the action, with a value of 5.

mapper()
    .wildcard()
.end();

Wheels 2 will only generate routes for [controller]/[action], however, because resources and the other routing methods are more appropriate for working with records identified by primary keys.

Here is a sample of the patterns that wildcard generates:

/news/new
/news/create
/news/index
/news

The wildcard method by default will only generate routes for the GET request method. If you would like to enable other request methods on the wildcard, you can pass in the method or methods argument:

mapper()
    .wildcard(methods="get,post")
.end();

Security Warning

Specifying a method argument to wildcard with anything other than get gives you the potential to accidentally expose a route that could change data in your application with a GET request. This opens your application to Cross Site Request Forgery (CSRF) vulnerabilities.

wildcard is provided for convenience. Once you're comfortable with routing concepts in Wheels, we strongly recommend that you use resources (resources, resource) and the other verb-based helpers (get, post, patch, put, and delete) listed above instead.

Order of Precedence

Wheels gives precedence to the first listed custom route in your app/config/routes.cfm file.

Consider this example to demonstrate when this can create unexpected issues:

mapper()
    .resources("users")

    .get(
        name="usersPromoted",
        pattern="users/promoted",
        to="userPromotions##index"
    )
.end();

In this case, when the user visits /users/promoted, this will load the show action of the users controller because that was the first pattern that was matched by the Wheels router.

To fix this, you need the more specific route listed first, leaving the dynamic routing to pick up the less specific pattern:

mapper()
    .get(
        name="usersPromoted",
        pattern="users/promoted",
        to="userPromotions##index"
    )

    .resources("users")
.end();

Making a Catch-All Route

Sometimes you need a catch-all route in Wheels to support highly dynamic websites (like a content management system, for example), where all requests that are not matched by an existing route get passed to a controller/action that can deal with it.

Let's say you want to have both /welcome-to-the-site and /terms-of-use handled by the same controller and action. Here's what you can do to achieve this.

First, add a new route to app/config/routes.cfm that catches all pages like this:

mapper()
    .get(name="page", pattern="[title]", to="pages##show")
.end();

Now when you access /welcome-to-the-site, this route will be triggered and the show action will be called on the pages controller with params.title set to welcome-to-the-site.

The problem with this is that this will break any of your normal controllers though, so you'll need to add them specifically before this route. (Remember the order of precedence explained above.)

You'll end up with a app/config/routes.cfm file looking something like this:

mapper()
    .resources("products")
    .get(name="logout", to="sessions#delete")
    .get(name="page", pattern="[title]", to="pages##show")
    .root(to="dashboards##show")
.end();

Constraints

The constraints feature can be added either at an argument level directly into a resources() or other individual route call, or can be added as a chained function in it's own right. Constraints allow you to add regex to a route matching pattern, so you could for instance, have /users/1/ and /users/bob/ go to different controller actions.

mapper()
   // users/1234
  .resources(name = "users", constraints = { key = "\d+" })
   // users/abc123
  .resources(name = "users", constraints = { key = "\w+\d+" })
.end();

Constraints can also be used as a wrapping function:

mapper()
    .constraints( key = "\d+")
        .resources("users")
        .resources("cats")
        .resources("dogs")
    .end()
.end()

In this example, the key argument being made of digits only will apply to all the nested resources

Wildcard Segments

Wildcard segments allow for wildcards to be used at any point in the URL pattern.

mapper()
  // Match /user/anything/you/want
  .get(name="user/*[username]", to="users##search")
  // Match /user/anything/you/want/search/
  .get(name="user/*[username]/search", to="users##search")
.end()

In the above example, anything/you/want you gets set to the params.username including the /'s. The second example would require /search/ to be on the end of the URL

Shallow Resources

If you have a nested resource where you want to enforce the presence of the parent resource, but only on creation of that resource, then shallow routes can give you a bit of a short cut. An example might be Blog Articles, which have Comments. If we're thinking in terms of our models, let's say that Articles Have Many Comments.

mapper()
    .resources(name="articles", nested=true, shallow=true)
        .resources("comments")
        .resources("quotes")
        .resources("drafts")
    .end()
.end()

Without shallow routes, this block would create RESTful actions for all the nested resources, for example /articles/[articleKey]/comments/[key]

With Shallow resources, we can automatically put the index, create and new RESTful actions with the ArticleKey in the URL, but then separate out edit, show, update and delete actions into their own, and simpler URL path; When we edit or update a comment, we're doing it on that object as it's own entity, and the relationship to the parent article already exists.

So in this case, we get index, new and create with the /articles/[articleKey]/ part in the URL, but to show, edit, update or delete a comment, we can just fall back to /comments/

Member and Collection Blocks

A member() block is used within a nested resource to create routes which act 'on an object'; A member route will require an ID, because it acts on a member. photos/1/preview is an example of a member route, because it acts on (and displays) a single object.

mapper()
    // Create a route like `photos/1/preview`
    .resources(name="photos", nested=true)
        .member()
            .get("preview")
        .end()
    .end()
.end();

A collection route doesn't require an id because it acts on a collection of objects. photos/search is an example of a collection route, because it acts on (and displays) a collection of objects.

mapper()
    // Create a route like `photos/search`
    .resources(name="photos", nested=true)
        .collection()
            .get("search")
        .end()
    .end()
.end();

Redirection

As of Wheels 2.1, you can now use a redirect argument on GET, POST, PUT, PATCH, and DELETE requests. This will execute before reaching any controllers, and perform a 302 redirect immediately after the route is matched.

CFScript

mapper()
  .get(name="foo", redirect="https://www.google.com")
  .post(name="foo", redirect="https://www.google.com")
  .put(name="foo", redirect="https://www.google.com")
  .patch(name="foo", redirect="https://www.google.com")
  .delete(name="foo", redirect="https://www.google.com")
.end()

This is useful for the occasional redirect, and saves you having to create a dedicated controller filter or action just to perform a simple task. For large amounts of redirects, you may want to look into adding them at a higher level - e.g in an Apache VirtualHost configuration, as that will be more performant.

Disabling automatic [format] routes

Note

Introduced in Wheels 2.1

By default, Wheels will add .[format] routes when using resources(). You may wish to disable this behavior to trim down the number of generated routes for clarity and performance reasons (or you just don't use this feature!).

You can either disable this via mapFormat = false on a per resource basis, or more widely, on a mapper basis:

// For all chained calls
mapper(mapFormat=false)
.resources("users")
.end()

// or just for this resource
mapper()
.resources(mapFormat=false, name="users)
.end()

\

Verification

Verify the existence and type of variables before allowing for a given controller action to run.

Using verifies() to Enforce Request Types

All that you need to do is add this line to your controller's config() function:

// In the controller
function config(){
    verifies(only="create,update", post=true);
}

The code above will ensure that all requests coming to the create and update actions are from form submissions. All other requests will be aborted.

There are also boolean arguments for get and ajax request types.

Defining a Handler for Failed Verifications

You can also specify different behavior for when the verification fails in a special handler function registered with the handler argument.

In this example, we register the incorrectRequestType() function as the handler:

// In the controller
function config(){
    verifies(
        only="create,update",
        post=true,
        handler="incorrectRequestType";
}  
    
function incorrectRequestType(){
    redirectTo(action="accessDenied");
}

Enforcing the Existence and Type of Variables

Step back in time for a moment and remember how you used to code websites before Wheels. (Yes I know those were dark days, but stay with me.)

On your edit.cfm page, what you probably did was write some code at the top of that looked like this:

<cfif !StructKeyExists(form, "userId") OR !IsValid("guid", form.userId)>
    <cflocation url="index.cfm" addToken="false">
</cfif>

With this snippet of code, you could ensure that any request to the edit.cfm had to have the userId in the form scope and that userId had to be of type guid. If these conditions weren't met, the request was redirected to the index.cfm page. This was a very tedious but necessary task.

// In the controller 
function config(){
   verifies(
        only="edit",
        post=true,
        params="userId",
        paramsTypes="guid",
        action="index",
        error="Invalid user ID."
    );
}

With that one line of code, Wheels will perform the following checks when a request is made to the controller's editaction:

  • Make sure that the request is a post request.

  • Make sure that the userId variable exists in the params struct.

  • Make sure that the params.userId is of typeguid.

  • If any of those checks fail, redirect the request to the index action and place an error key in the Flash containing the message "Invalid user ID."

All that functionality in only one line of code!

What if you wanted do this for two or more variables? The params and paramsTypes each accept a list so you can include as many variables to check against as you want.

The only thing you need to make sure of is that the number of variables in the params list matches the number types to check against in the paramsTypes list. This also goes for the session/sessionTypes and cookie/cookieTypesarguments, which check for existence and type in the session and cookie scopes, respectively.

Controller Verification vs. Model Object Validation

A basic example of this is to validate params passed through to your controller from routes. Suppose we have the following route in our application:

.post(name="usersAddressesSave",
      pattern="admin/users/[userid]/addresses/save",
      to="UserAddresses##save")

In this example, we will want to verify that the userId integer and address struct are both present in the paramsstruct and also that userId is of a certain type:

verifies(
    only="save",
    post=true,
    params="userId,address",
    paramsTypes="integer,struct"
)>

Event Handlers

Use the standard CFML application events through the framework.

While it's perfectly possible to add your code directly to the public/Application.cfc or vendor/wheels/Global.cfc file, we certainly don't recommend it. If you add code in there, you both increase the risk of accidentally modifying how the framework functions, and you also make it a lot harder to upgrade to future versions of Wheels.

Use the events Folder for Standard CFML Events

The general recommendation is to never touch any files in the wheels folder. OK, with that little warning out of the way, how does one go about using the CFML events?

The answer is to use the app/events folder. There is a file in there for every single event that CFML triggers. So if you want some code executed on application start for example, just place your code in onapplicationstart.cfm, and Wheels will run it when your application starts.

Wheels Includes Some Extra Bonus Events

If you look closely in the events folder, you will also notice that there are some custom files in there that do not match up with standard CFML events. The onmaintenance.cfm file is one example. Let's have a closer look at these.

On Maintenance

The onmaintenance.cfm file is included when Wheels is set to maintenance mode. After the file is included, cfabortis called by Wheels so no other code runs in this mode.

On Error

You can place a generic error message in the onerror.cfm file to be displayed to the users whenever your site throws an error.

If you need to access the error information here (for logging purposes, for example) it is available at arguments.exception.

On Missing Template

The onmissingtemplate.cfm file works in a similar way as the error file above, but it gets called when a controller or action in your application could not be found.

Note: If you want to make sure that all browsers show your custom 404 page you need to make it larger than 512 bytes in size. Google Chrome, for example, will display a friendly help page of its own when the 404 page is less than 512 bytes.

Adding Functions

Sometimes it's useful to add functions right in the Application.cfc file to make them available to all templates. To achieve the same thing in Wheels, you can place your functions in /app/global/functions.cfm.

Application Settings

Again, because there is no Application.cfc file for you to work with in Wheels, you have to find a suitable place to set application settings such as SessionManagement, SessionTimeout, ScriptProtect, SetClientCookies, and so on. These are usually set in the constructor area of an Application.cfc file. We recommend that you set them in the app/config/app.cfm file instead.

IIS

URL rewriting instructions for IIS

Instructions for IIS 7

Similar to Apache, IIS 7 will pick up the rewrite rules from a file located in the Wheels installation. In the case of IIS 7, the rules are picked up by adding the following web.config file.

web.config
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="ColdFusion on Wheels URL Rewriting" enabled="true">
                    <match url="^(.*)$" ignoreCase="true" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{SCRIPT_NAME}" negate="true" pattern="^/(flex2gateway|jrunscripts|cf_scripts|cfide|CFFileServlet|cfformgateway|lucee|files|images|javascripts|miscellaneous|stylesheets|wheels/public/assets|robots.txt|favicon.ico|sitemap.xml|rewrite.cfm)($|/.*$)" />
                    </conditions>
                    <action type="Rewrite" url="/rewrite.cfm/{R:1}" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

Missing Lucee Assets?

If you had an issue with missing Lucee CSS files, try changing{SCRIPT_NAME}to{PATH_INFO}in the code above, as this reportedly can resolve the issue.

Instructions for IIS 6

Deprecated

Please note that IIS6 was official End of Life as of 2015. These notes are included for historical purposes only.

Unfortunately, there is no built-in URL rewriting mechanism in IIS 6, so getting Wheels working with pretty URLs is a little more complicated than with Apache and IIS 7 (which often comes with the official "URL Rewrite Module" installed by default). Here's what you need to do:

  • Unzip the file, get the IsapiRewrite4.dll file from the lib folder and put it in the root of your website. (It needs to be in the same folder as the IsapiRewrite4.ini file.)

  • To enable the rewrite filter in IIS 6, click on Properties for your website, then go to the ISAPI Filters tab and click the Add... button.

  • Type in anything you want as the Filter Name and point the Executable to the IsapiRewrite4.dll file.

  • Uncomment the rewrite rules in the IsapiRewrite4.ini file.

NOTE: Make sure you have "Verify that file exists" disabled for your site.

  • Right click your website and select Properties.

  • Click Home Directory tab.

  • Click the Configuration button.

  • Under the Wildcard application mapping section, double-click path for the jrun_iis6_wildcard.dll.

  • Uncheck Verify that file exists.

  • Click OK until all property screens are closed.

Tomcat

URL rewriting instructions for Tomcat

Using rewrite Valve

Instructions for UrlRewriteFilter

UrlRewriteFilter (commonly referred to as Tuckey) is a Java web filter for compliant web application servers such as Tomcat, Jetty, Resin and JBoss. Unfortunately UrlRewriteFilter depends on XML with its extremely strict syntax.

  • Append the servlet-mapping markup to the end of the <filter mapping> element in your WEB-INF/web.xml

  • Add the pretty urls rule markup to the <urlrewrite> element to your WEB-INF/urlrewrite.xml configuration.

  • Restart the web application server.

Servlet-Mapping markup

servlet-mapping
<servlet-mapping>
  <servlet-name>CFMLServlet</servlet-name>
    <url-pattern>/rewrite.cfm/*</url-pattern>
</servlet-mapping>

Example markup with UrlRewriteFilter and Wheels pretty URLs for WEB-INF/web.xml.

web.xml
<filter>
    <filter-name>UrlRewriteFilter</filter-name>
    <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>UrlRewriteFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
<servlet-mapping>
  <servlet-name>CFMLServlet</servlet-name>
    <url-pattern>/rewrite.cfm/*</url-pattern>
</servlet-mapping>

Pretty URLs Rule markup

pretty urls rule
<rule enabled="true">
    <name>Wheels pretty URLs</name>
    <condition type="request-uri" operator="notequal">^/(flex2gateway|jrunscripts|cfide|cf_scripts|cfformgateway|cffileservlet|lucee|files|images|javascripts|miscellaneous|stylesheets|wheels/public/assets|robots.txt|favicon.ico|sitemap.xml|rewrite.cfm)</condition>
    <from>^/(.*)$</from>
    <to type="passthrough">/rewrite.cfm/$1</to>
  </rule>

A complete barebones WEB-INF/urlrewrite.xml configuration example with pretty URLs.

urlrewrite.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite
    PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN"
    "http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd">

<urlrewrite>
  <rule enabled="true">
    <name>Wheels pretty URLs</name>
    <condition type="request-uri" operator="notequal">^/(flex2gateway|jrunscripts|cfide|cf_scripts|cfformgateway|cffileservlet|lucee|files|images|javascripts|miscellaneous|stylesheets|wheels/public/assets|robots.txt|favicon.ico|sitemap.xml|rewrite.cfm)</condition>
    <from>^/(.*)$</from>
    <to type="passthrough">/rewrite.cfm/$1</to>
  </rule>
</urlrewrite>

URL Rewriting

Making URLs prettier using URL rewriting.

URL rewriting is a completely optional feature of Wheels, and all it does is get rid of the index.cfm part of the URL.

For example, with no URL rewriting, a URL in your application could look like this:

HTTP
http://localhost/index.cfm/blog/new

After turning on URL rewriting, it would look like this:

HTTP
http://localhost/blog/new

Combine this with the routing functionality of Wheels, and you get the capability of creating some really human-friendly (easier to remember, say over the phone, etc.) and search engine friendly (easier to crawl, higher PageRank, etc.) URLs.

Once you have added the rewrite rules (usually in either .htaccess, web.config or urlrewrite.xml), Wheels will try and determine if your web server is capable of rewriting URLs and turn it on for you automatically. Depending on what web server you have and what folder you run Wheels from, you may need to tweak things a little though. Follow these instructions below for details on how to set up your web server and customize the rewrite rules when necessary.

Head's Up!

Since 2.x, engine specific URL rewrite files are not included in the default distribution. Don't worry - we've got you covered though!

For webserver specific instructions look at the following pages:

Don't Forget to Restart

If you need to make changes to get URL rewriting to work, it's important to remember to always restart the web server and the ColdFusion server to make sure the changes are picked up by Wheels.

If you don't have access to restart services on your server, you can issue a reload=true request. It's often enough.

Pages

Where to place your view files and what to put in them.

In this chapter, we'll explain exactly where to place these files and what to put in them.

Where to Place the Files

Some rules can be spotted here:

  • All view files live in the app/views folder.

  • Each controller gets a subfolder named after it in the app/views folder.

  • The view file to include is just a regular .cfm file named after the action.

For creating standard pages, your work process will likely consist of the following steps:

  1. Create the controller action (a function in the controller CFC file).

  2. Create the corresponding view file for it (a .cfm file in the controller's view folder).

There can be some exceptions to this process though, so let's go through some possible scenarios.

Controller Actions Without Associated View Files

Rendering the View File for Another Action

Sometimes you want the controller action to render the view file for a different action than the one currently executing. This is especially common when your application processes a form and the user makes an input error. In this case, you'll probably choose to have your application display the same form again for correction.

Sharing a View File Between Actions

When using the template argument, there are specific rules that Wheels will follow in order to locate the file you want to include:

  • If the template argument starts with the / character, Wheels will start searching from the app/views folder. Example: renderView(template="/common/page") will include the app/views/common/page.cfm file.

  • If it contains the / character elsewhere in the string, the search will start from the controller's view folder. Example: renderView(template="misc/page") will include the app/views/blog/misc/page.cfm file if we're currently in the blog controller.

  • In all other cases (i.e. when the template argument does not contain the / character at all), Wheels will just assume the file is in the controller's view folder and try to include it. Example: renderView(template="something") will include the app/views/blog/something.cfm file if we're currently in the blog controller.

Also note that both renderView(template="thepage") and renderView(template="thepage.cfm") work fine. But most of the time, Wheels developers will tend to leave out the .cfm part.

What Goes in the Files?

This is the output of your application: what the users will see in their browsers. Most often this will consist of HTML, but it can also be JavaScript, CSS, XML, etc. You are of course free to use any CFML tags and functions that you want to in the file as well. (This is a CFML application, right?)

When writing your view code, you will have access to the variables you have set up in the controller file. The idea is that the variables you want to access in the view should be set unscoped (or in the variables scope if you prefer to set it explicitly) in the controller so that they are available to the view template.

In addition to the variables you have set yourself, you can also access the params struct. This contains anything passed in through the URL or with a form. If you want to follow MVC rules more closely though, we recommend only accessing the params struct in the controller and then setting new variables for the information you need access to in the view.

The most important thing to remember when creating your view is to be careful not to put too much code in there. Avoid code dealing with the incoming request (this can be moved to the controller) and code containing business logic (consider moving this to a model). If you have view-related code but too much of it, it may be beneficial to break it out into a helper or a partial.

Cleaning Up Output

A view's job is also to clean up and format the values provided by the controller before being displayed. This is especially important when content from a data source is not HTML-encoded.

For example, if the view is to display the title column from a query object called posts, it should encode HTML special characters:

<ul>
    <cfoutput query="posts">
        <li>#EncodeForHtml(posts.title)#</li>
    </cfoutput>
</ul>

Please note that you do not need to do this when passing in data to Wheels view helpers. The view helpers themselves will handle calling EncodeForUrl, EncodeForHtml and EncodeForHtmlAttribute internally as needed.

To control encoding in general you have three global settings at your disposal (they all default to true):

Partials

Simplify your views by breaking them down into partial page templates.

These functions also add a few cool things to your development arsenal like the ability to pass in a query or array of objects and have the partial file called on each iteration (to name one).

Why Use a Partial?

Even when there is no risk of code duplication, it can still make sense to use a partial. Breaking up a large page into smaller, more manageable chunks will help you focus on each part individually.

If you've used <cfinclude> a lot in the past (and who hasn't?!), you probably already knew all of this though, right?

Storing Your Partial Files

Making the Call

#includePartial("banner")#

That code will look for a file named _banner.cfm in the current controller's view folder and include it.

Let's say we're in the blog controller. Then the file that will be included is app/views/blog/_banner.cfm.

As you can see, you don't need to specify the .cfm part or the underscore when referencing a partial.

Passing in Data

Here is an example of passing in a title to a partial for a form:

#includePartial(partial="loginRegisterForm", title="Please log in here")#

Now you can reference the title variable as arguments.title inside the _loginregisterform.cfm file.

If you prefer, you can still access the view variables that are set outside of the partial. The advantage with specifically passing them in instead is that they are then scoped in the arguments struct (which means less chance of strange bugs occurring due to variable conflicts). It also makes for more readable and maintainable code. (You can see the intent of the partial better when you see what is passed in to it).

Special arguments

The query, object and objects arguments are special arguments and should not be used for other purposes than what's documented in the sections further down on this page.

Automatic Calls to a Data Function

There is an even more elegant way of passing data to a partial though. When you start using a partial on several pages on a site spread across multiple controllers, it can get quite cumbersome to remember to first load the data in an appropriate function in the controller, setup a before filter for it, pass that data in to the partial, and so on.

Wheels can automate this process for you. The convention is that a partial will always check if a function exists on the controller with the same name as the partial itself (and that it's set to private and will return a struct). If so, the partial will call the function and add the output returned to its arguments struct.

This way, the partial can be called from anywhere and acts more like a "black box." All communication with the model is kept in the controller as it should be for example.

Partials with Layouts

Just like a regular page, Wheels partials also understand the concept of layouts. To use this feature, simply pass in the name of the layout file you want to wrap the partial in with the layout argument, like this:

#includePartial(partial="newsItem", layout="/boxes/blue")#

That said, your _blue.cfm file could end up looking something like this:

<div class="news">
    #includeContent()#
</div>

One difference from page layouts is that the layout file for partials has to start with the underscore character.

It's also worth noting that it's perfectly acceptable to include partials inside layout files as well. This opens up the possibility to nest layouts in complex ways.

Caching a Partial

Caching a partial is done the same way as caching a page. Pass in the number of minutes you want to cache the partial for to the cache argument.

Here's an example where we cache a partial for 15 minutes:

#includePartial(partial="userListing", cache=15)#

Using Partials with an Object

Because it's quite common to use partials in conjunction with objects and queries, Wheels has built-in support for this too. Have a look at the code below, which passes in an object to a partial:

cust = model("customer").findByKey(params.key);
#includePartial(cust)#

That code will figure out that the cust variable contains a customer model object. It will then try to include a partial named _customer.cfm and pass in the object's properties as arguments to the partial. There will also be an objectvariable available in the arguments struct if you prefer to reference the object directly.

Try that code and <cfdump> the arguments struct inside the partial file, and you'll see what's going on. Pretty cool stuff, huh?

Using Partials with a Query

customers = model("customer").findAll();
#includePartial(partial="customers", query=customers)#

In this case, Wheels will iterate through the query and call the _customer.cfm partial on each iteration. Similar to the example with the object above, Wheels will pass in the objects' properties (in this case represented by records in the query) to the partial.

In addition to that, you will also see that a counter variable is available. It's named current and is available when passing in queries or arrays of objects to a partial.

The way partials handle objects and queries makes it possible to use the exact same code inside the partial regardless of whether we're dealing with an object or query record at the time.

If you need to display some HTML in between each iteration (maybe each iteration should be a list item for example), then you can make use of the spacer argument. Anything passed in to that will be inserted between iterations. Here's an example:

<ul>
    <li>#includePartial(partial=customers, query=customers, spacer="</li><li>")#</li>
</ul>

Partials and Grouping

There is a feature of CFML that is very handy: the ability to output a query with the group attribute. Here's an example of how this can be done with a query that contains artists and albums (with the artist potentially being duplicated since they can have more than one album):

<cfoutput query="artistsAndAlbums" group="artistid">
    <!--- Artist info is displayed just once for each artist here --->
    <cfoutput>
        <!--- Each album is looped here --->
    </cfoutput>
</cfoutput>

We have ported this great functionality into calling partials with queries as well. Here's how you can achieve it:

#includePartial(partial=artistsAndAlbums, query=artistsAndAlbums, group="artistId")#

When inside the partial file, you'll have an additional subquery made available to you named group, which contains the albums for the current artist in the loop.

Using Partials with an Array of Objects

As we've hinted previously in this chapter, it's also possible to pass in an array of objects to a partial. It works very similar to passing in a query in that the partial is called on each iteration.

Rendering or Including?

Let's say that you want to submit comments on your blog using AJAX. For example, the user will see all comments, enter their comment, submit it, and the comment will show up below the existing ones without a new page being loaded.

Here's what your controller action that receives the AJAX form submission would look like:

comment = model("comment").create(params.newComment);
renderPartial(comment);

Please note that there currently is no support for creating the AJAX form directly with Wheels. This can easily be implemented using a JavaScript library such as jQuery or Prototype though.

Using Filters

Stop repeating yourself with the use of before and after filters.

If you find the need to run a piece of code before or after several controller actions, then you can use filters to accomplish this without needing to explicitly call the code inside each action in question.

This is similar to using the onRequestStart / onRequestEnd functions in CFML's Application.cfc file, with the difference being that filters tie in better with your Wheels controller setup.

An Example: Authenticating Users

One common thing you might find yourself doing is authenticating users before allowing them to see your content. Let's use this scenario to show how to use filters properly.

You might start out with something like this:

component extends="Controller" {

  function secretStuff() {
        if ( !StructKeyExists(session, "userId") ) {
            abort;
        }
    }

    function evenMoreSecretStuff() {
        if ( !StructKeyExists(session, "userId") ) {
            abort;
        }
    }

}

Sure, that works. But you're already starting to repeat yourself in the code. What if the logic of your application grows bigger? It could end up looking like this:

component extends="Controller" {

    function secretStuff() {
        if ( !find("212.55", cgi.remote_addr) ) {
            flashInsert(alert="Sorry, we're !open in that area.");
            redirectTo(action="sorry");
        } else if ( !StructKeyExists(session, "userId") ) {
            flashInsert(alert="Please login first.");
            redirectTo(action="login");
        }
    }

    function evenMoreSecretStuff() {
        if ( !find("212.55", cgi.remote_addr) ) {
            flashInsert(msg="Sorry, we're !open in that area.");
            redirectTo(action="sorry");
        } else if ( !StructKeyExists(session, "userId") ) {
            flashInsert(msg="Please login first.");
            redirectTo(action="login");
        }
    }

}

Ouch! You're now setting yourself up for a maintenance nightmare when you need to update that IP range, the messages given to the user, etc. One day, you are bound to miss updating it in one of the places.

As the smart coder that you are, you re-factor this to another function so your code ends up like this:

component extends="Controller" {

    function secretStuff() {
        restrictAccess();
    }

    function evenMoreSecretStuff() {
        restrictAccess();
    }

    function restrictAccess() {
        if ( !find("212.55", cgi.remote_addr) ) {
            flashInsert(msg="Sorry, we're !open in that area.");
            redirectTo(action="sorry");
        } else if ( !StructKeyExists(session, "userId") ) {
            flashInsert(msg="Please login first!");
            redirectTo(action="login");
        }
    }

}
component extends="Controller" {

    function config() {
        filters("restrictAccess");
    }

    private function restrictAccess() {
        if ( !find("212.55", cgi.remote_addr) ) {
            flashInsert(msg="Sorry, we're !open in that area.");
            redirectTo(action="sorry");
        } else if ( !StructKeyExists(session, "userId") ) {
            flashInsert(msg="Please login first!");
            redirectTo(action="login");
        }
    }

    function secretStuff() {
    }

    function evenMoreSecretStuff() {
    }

}

Besides the advantage of not having to call restrictAccess() twice, you have also gained two other things:

  • The developer coding secretStuff() and evenMoreSecretStuff() can now focus on the main tasks of those two actions without having to worry about repetitive logic like authentication.

  • The config() function is now starting to act like an overview for the entire controller.

All of these advantages will become much more obvious as your applications grow in size and complexity. This was just a simple example to put filters into context.

Sharing Filters Between Controllers

So far, we've only been dealing with one controller. Unless you're building a very simple website, you'll end up with a lot more.

If you actually want to set the same filters to be run for all controllers, you can go ahead and move it to the Controller.cfc file's config() function as well. Keep in mind that if you want to run the config() function in the individual controller and in Controller.cfc, you will need to call super.config() from the config() function of your individual controller.

Two Types of Filters

The previous example with authentication showed a "before filter" in action. The other type of filter you can run is an "after filter." As you can tell from the name, an after filter executes code after the action has been completed.

This can be used to make some last minute modifications to the HTML before it is sent to the browser (think translation, compression, etc.), for example.

As an example, let's say that you want to translate the content to Gibberish before sending it to your visitor. You can do something like this:

function config() {
    filters(through="translate", type="after");
}

private function translate() {
    setResponse(gibberify(response()));
}

Including and Excluding Actions From Executing Filters

By default, filters apply to all actions in a controller. If that's not what you want, you can tell Wheels to only run the filter on the actions you specify with the only argument. Or you can tell it to run the filter on all actions except the ones you specify with the except argument.

Here are some examples showing how to setup filtering in your controllers. Remember, these calls go inside the config() function of your controller file.

filters(through="isLoggedIn,checkIPAddress", except="home,login");
filters(through="translateText", only="termsOfUse", type="after");

Passing Arguments to Filter Functions

Sometimes it's useful to be able to pass through arguments to the filters. For one, it can help you reduce the amount of functions you need to write. Here's the easy way to pass through an argument:

filters(through="authorize", byIP=true);

Now the byIP argument will be available in the authorize function.

To help you avoid any clashing of argument names, Wheels also supports passing in the arguments in a struct as well:

// The `through` argument would clash here if it wasn't stored within a struct
args.byIP = true;
args.through = true;
filters(through="authorize", authorizeArguments=args);

Evaluating Filter Arguments at Runtime

Because your controller's config() function only runs once per application start, the passing of arguments can also be written as expressions to be evaluated at runtime. This is helpful if you need for the value to be dynamic from request to request.

For example, this code would only evaluate the value for request.region on the very first request, and Wheels will store that particular value in memory for all subsequent requests:

// This is probably not what you intended
filters(through="authorize", byIP=true, region=request.region);

To avoid this hard-coding of values from request to request, you can instead pass an expression. (The double pound signs are necessary to escape dynamic values within the string. We only want to store a string representation of the expression to be evaluated.)

// This is probably more along the lines of what you intended
filters(through="authorize", byIP=true, region="##request.region##");

Now instead of evaluating request.region inside the config() function, it will be done on each individual request.

Securing Your Filters

You probably don't want anyone to be able to run your filters directly (by modifying a URL on your website for example). To make sure that isn't possible we recommend that you always make them private. As you can see in all examples on this page we make sure that we always have access="private" in the function declaration for the filter.

Low Level Access

CORS Requests

Configure Wheels to properly handle CORS requests

Wheels can often act as the "backend" in a modern web application, serving data to multiple types of frontend clients. Typically this would be in the form of (but not limited to) JSON served as an API, with something like VueJS or React on the front end, possibly served under a different domain.

When we separate our systems in such a manner, we need to consider CORS (Cross Origin Resource Sharing) and how to properly serve requests which modern browsers will allow.

The "Quick and Dirty" approach

If you just need to satisfy your CORS requirement quickly, you can do so from Wheels 2.0 onwards with a simple configuration switch in your /app/config/settings.cfm file: set(allowCorsRequests=true);.

By default, this will enable the following CORS headers:

Access-Control-Allow-Origin 
*

Access-Control-Allow-Methods 
GET, POST, PATCH, PUT, DELETE, OPTIONS

Access-Control-Allow-Headers
Origin, Content-Type, X-Auth-Token, X-Requested-By, X-Requested-With

This will satisfy most requirements to get going quickly, but is more of a blanket "catch all" configuration which doesn't really restrict anything, or provide much information to the API consumer about your available resources.

Custom CORS Headers

From Wheels 2.1

The options below were introduced in Wheels 2.1

From Wheels 2.1, we can be more specific. We still need to specify set(allowCorsRequests=true); in our /app/config/settings.cfm to turn on the main CORS functionality, but we can now provide some additional configuration options to fine tune our responses.

Access Control Allow Origin

The Access Control Allow Origin header tells the browser whether the domain they are connecting from can access the requested resource.

By default, this header is set to a wildcard allowing connection from any domain. But it might be your VueJS app lives at app.domain.com and we only want to allow access from that domain to our API.

// Wildcard
set(accessControlAllowOrigin="*")

// Specify a domain
set(accessControlAllowOrigin="https://app.domain.com");

// Specify multiple domains in a list
set(accessControlAllowOrigin="https://app.domain.com,https://staging-app.domain.com");

You can also take advantage of the environment specific configurations, such as only allowing access to localhost:8080 in /app/config/development/settings.cfm for example.

Wheels 2.2 allows for subdomain wildcard matching for CORS permitted origins:

// Match https://foo.domain.com or https://bar.domain.com or https://www.mydomain.com
set(accessControlAllowOrigin = [
  "https://*.domain.com",
  "https://www.mydomain.com"
]);

Access Control Allow Methods

The Access Control Allow Methods tells the browser what HTTP Methods (Verbs) are allowed to be performed against the requested resource.

By default these are set to be all possible Methods, GET, POST, PATCH, PUT, DELETE, OPTIONS. If our API only allows specific methods, we can specify them: note that this is application-wide and not dependent on route.

cfscript

// Only ever allow GET requests to this API
set(accessControlAllowMethods="GET");

// Only ever allow GET, POST and OPTIONS
set(accessControlAllowMethods="GET,POST,OPTIONS");

Access Control Allow Methods (By Route)

Whilst setting Access Control Allow Methods site-wide is fine, it doesn't actually fulfill the CORS requirement properly - the value returned by this header should indicate what methods are available at that url. For instance, /cats might only allow GET,POST requests, and /cats/1/ might only allow GET,PUT,PATCH,DELETE requests.

Thankfully, we can pull this information in from the routing system automatically! Note, set(accessControlAllowMethodsByRoute=true) will override set(accessControlAllowMethods())

// automatically look up the available routes in application.wheels.routes and return the valid methods for the requested route
set(accessControlAllowMethodsByRoute=true);

Access Control Allow Credentials

If you're sending credentials such as a cookie from your front end application, you may need to turn this header on.

// if set to true, include the Access-Control-Allow-Credentials header
set(accessControlAllowCredentials=true);

Access Control Allow Headers

If you need to specify a specific list of allowed headers, you can simply pass them into this configuration setting

// Set site wide allowed headers
set(accessControlAllowHeaders = "Origin, Content-Type, X-Auth-Token, X-Requested-By, X-Requested-With, X-MyHeader")

Caching

How to speed up your website by caching content.

If your website doesn't get a whole lot of traffic, then you can probably skip this chapter completely. Just remember that it's here, waiting for your triumphant return.

On the other hand, you're probably hoping for massive amounts of traffic to reach your website very soon, an imminent sell-out to Google, and a good life drinking Margaritas in the Caribbean. Knowing a little bit about the Wheels caching concepts will prepare you for this. (Except for the Margaritas... You're on your own with those ;).)

Consider a typical page on a Wheels website. It will likely call a controller action, get some data from your database using a finder method, and render the view for the user using some of the handy built-in functions.

All this takes time and resources on your web and database servers. Let's assume this page is accessed frequently, rarely changes, and looks the same for all users. Then you have a perfect candidate for caching.

Configuring the Cache

Wheels will configure all caching parameters behind the scenes for you based on what environment mode you are currently in. If you leave everything to Wheels, caching will be optimized for the different environment modes (and set to have reasonable settings for cache size and culling). This means that all caching is off when working in Development mode but on when in Production mode, for example.

Here are the global caching parameters you can set, their default values, and a description of what they do. Because these are not meant to be set differently based on the environment mode, you would usually set these in the app/config/settings.cfm file:

set(maximumItemsToCache=5000);
set(cacheCullPercentage=10);
set(cacheCullIntervalMinutes=5);
set(cacheDatePart="n");
set(defaultCacheTime=60);
set(cacheQueriesDuringRequest=true);
set(clearQueryCacheOnReload=true);
set(cachePlugins=true);

maximumItemsToCache Setting

The total amount of items the cache can hold. When the cache is full, items will be deleted automatically at a regular interval based on the other settings below.

Note that the cache is stored in ColdFusion's application scope, so take this into consideration when deciding the number of items to store.

cacheCullPercentage Setting

This is the percentage of items that are culled from the cache when maximumItemsToCache has been reached.

For example, if you set this value to 10, then at most, 10% of expired items will be deleted from the cache.

If you set this to 100, then all expired items will be deleted. Setting it to 100 is perfectly fine for small caches but can be problematic if the cache is very large.

cacheCullInterval Setting

This is the number of minutes between each culling action. The reason the cache is not culled during each request is to keep performance as high as possible.

cacheDatePart Setting

The measurement unit to use during caching. The default is minutes ("n"), but you can set it to seconds ("s") for example if you want more precise control over when a cached item should expire. For the rest of this chapter, we'll assume you've left it at the default setting.

defaultCacheTime Setting

The number of minutes an item should be cached when it has not been specifically set through one of the functions that perform the caching in Wheels (i.e., caches(), findAll(), includePartial(), etc).

cacheQueriesDuringRequest Setting

When set to true, Wheels caches identical queries during a request. See the section titled Automatic Caching of Identical Queries below for more information about this setting.

clearQueryCacheOnReload Setting

When set to true, Wheels will clear out all cached queries when doing a reload=true request. This is usually a good idea, but if you want to avoid hitting the database too hard on application reloads, you can set this to false to keep queries cached and ease the load on the database.

cachePlugins Setting

If you set this to false, all plugins will reload on each request (as opposed to during reload=true requests only). Setting this is only recommended if you are developing a plugin yourself and need to see the impact of your changes as you develop it on a per-request basis.

Environment-Level Caching

The following cache variables are usually set per environment mode:

// always turned on regardless of mode setting.
set(cacheControllerConfig = true);
set(cacheDatabaseSchema = true);
set(cacheModelConfig = true);
set(cachePlugins = true);

// Development mode example
set(cacheActions = false);
set(cacheFileChecking = false);
set(cacheImages = false);
set(cachePages = false);
set(cachePartials = false);
set(cacheQueries = false);

The settings shown above are what's in use in the Development mode. As you can see, when running in that mode, very little is cached. This makes it possible to do extensive changes without having to issue a reload=true request or restart the ColdFusion server, for example. The downside is that it makes for slightly slower development because the pages will not load as fast in this environment.

The variables themselves are fairly self-explanatory. When cacheDatabaseSchema is set to false, you can add a field to the database, and Wheels will pick that up right away. When cacheModelConfig is false, any changes you make to the config() function in the model file will be picked up. And so on.

No more Design mode

Note that "Design" mode has been removed in Wheels 2.x: please use development mode instead.

4 Ways to Cache

In Wheels, you can cache data in 4 different ways:

  • Action caching is the most effective of these methods because it caches the entire resulting HTML for the user.

  • Page caching is similar to action caching, but it will only cache the view page itself and in reality, this caching method is rarely used.

  • Partial caching is used when you only want to cache specific parts of pages. One reason for this could be that the page is personalized for a specific user, and you can only cache the sections that are not personalized.

  • Query caching is the least effective of the 4 caching options because it only caches result sets that you get back from the database. But if you have a busy database and you're not too concerned with leaving pages/partials uncached, this could be a good option for you.

Action Caching

This code specifies that you want to cache the browseByUser and browseByTitle actions for 30 minutes:

Caches Example
component extends="Controller" {

   function config(){
     caches(actions="browseByUser,browseByTitle", time=30);
   }

   function browseByUser(){
     }

   function browseByTitle(){
     }
}

As you can see, the caches() function call goes inside the config() function at the top of your controller file and accepts a list of action names and the number of minutes the actions should be cached for.

So what happens when users request the browseByUser page?

When Wheels receives the first request for this page, it will handle everything as it normally would, with the exception that it also adds a copy of the resulting HTML to the cache before ending the processing.

Wheels creates an internal key for use in the cache as well. This key is created from the controller, action, key, and params variables. This means, for example, that paginated pages are all stored individually in the cache (because the URL variable for the page to display would be different on each request).

When the second user requests the same page, Wheels will serve the HTML directly from the cache.

All subsequent requests now get the cached page until it expires.

One way to use this feature is to submit your forms to the same page to have it re-created or redirect to the cached page with a message in the Flash.

Here is some code that shows this technique with using the Flash to expire the cache. (Imagine that the showArticlepage is cached and a user is adding a new comment to it.)

Expire with Flash
flashInsert(message="Your comment was added");
redirectTo(action="showArticle", key=params.key);

Page Caching

This code specifies that you want to cache the view page for the browseByUser action for 1 hour:

browseByUser caches
component extends="Controller" {

   function browseByUser(){
     renderView(cache=60);
     }

   function browseByTitle(){
     }
}

The difference between action caching and page caching is that page caching will run the action and then only cache the view page itself. Action caching, as explained above, does not run the action code at all (but it does run filters and verifications).

Partial Caching

When your site contains personalized information (maybe some text specifying who you are logged in as, a list of items in your shopping cart, etc.), then action caching is not an option, and you need to cache at a lower level. This is where being able to cache only specific parts of pages comes in handy.

If you just pass in true, the default cache expiration time will be used.

So, for example, if you have an e-commerce site that lists products with a shopping cart in the sidebar, then you'd create a partial for the list of products and cache only that.

Example code:

IncludePartial
#includePartial(partial="listing", cache=true)#

Behind the scenes Wheels creates the cache key based on the name of the partial plus all of the arguments passed in to it. This means that if you pass in a userId variable for example, they will be cached separately.

You can also use this to your advantage when it comes to expiring cached content. You could, for example, update an updatedAt property on a user object and pass that in to includePartial. As soon as it changes, Wheels will recreate the cached content (since the cache key is now different).

Query Caching

You can cache result sets returned by your queries too. As a ColdFusion developer, this probably won't be new to you because you've always been able to use the cachedwithin attribute in the <cfquery> tag. The query caching in Wheels is very similar to this.

You can use query caching on all finder methods, and it looks like this:

users = model("user").findAll(where="name LIKE 'a%'", cache=10);

So there you have it: 4 easy ways to speed up your website!

Automatic Caching of Identical Queries

When working with objects in Wheels, you'll likely find yourself using all of the convenient association functions Wheels makes available to you.

For example, if you have set up a belongsTo association from article to author, then you will likely write a lot of article.author().name() calls. In this case, Wheels is smart enough to only call the database once per request if the queries are identical. So don't worry about adding performance hits when making multiple calls like that in your code.

The caching of these queries are stored on a per-model basis and Wheels will conveniently clear the cache for a model whenever anything changes in the database for a model (e.g. you call something that creates, updates or deletes records).

Obfuscating URLs

Hide your primary key values from nosy users.

The Wheels convention of using an auto-incrementing integer value as the primary key in your database tables will lead to a lot of URLs on your website exposing this value. Using the built-in URL obfuscation functionality in Wheels, you can hide this value from nosy users.

What URL Obfuscation Does

When URL obfuscation is turned off (which is the default setting in Wheels), this is how a lot of your URLs will end up looking:

HTTP
http://localhost/user/profile/99

Here, 99 is the primary key value of a record in your users table.

After enabling URL obfuscation, this is how those URLs will look instead:

HTTP
http://localhost/user/profile/b7ab9a50

The value 99 has now been obfuscated by Wheels to b7ab9a50. This makes it harder for nosy users to substitute the value to see how many records are in your users table, to name just one example.

How to Use It

To turn on URL obfuscation, all you have to do is call set(obfuscateURLs=true) in the app/config/settings.cfm file.

Is This Really Secure?

No, this is not meant to add a high level of security to your application. It just obfuscates the values, making casual observation harder. It does not encrypt values, so keep that in mind when using this approach.

For instance, unless you specify it in your app/config/routes.cfm file, you can still directly access numeric keys in the URL, e.g. /users/view/99; However, there is a small work around you can implement to prevent this at least, using the routes constraints argument.

mapper()
  .resources(name = "users", constraints = { key = "\w+\d+" } )
  .root( to="home##index", method="get")
.end();

This uses Regex to ensure the params.key argument is an alpha numeric key and not just purely numeric: otherwise the route won't match.

Localization

Work in progress

This chapter is still being constructed...

Page Encoding Issues

Generally speaking, if you try and add Unicode characters such as umlauts into templates, you may well come across display issues. This is easily fixable, but requires one of the following:

  • For the template.cfm file to be saved in the correct encoding for the language being displayed

  • Or use of the cfprocessingdirective tag to set pageEncoding

Using cfprocessingdirective

Incorrect encoding example

Correct encoding

Likewise, umlauts in routes would need for the app/config/routes.cfm file to have the correct encoding:

Using file encoding

If you're actively trying to avoid the use of cfprocessingdirective, you can resave the template or route file with UTF-8-BOM. Your local text editor should provide this facility; here's an example in Notepad++ (windows)

Localizing Wheels Helpers

// Example using monthNames args in dateSelect() Coming soon

Form Helpers and Showing Errors

Wheels ties your application's forms together with your model layer elegantly. With Wheels form conventions, you'll find yourself spending less time writing repetitive markup.

The majority of applications are not all about back-end. There is a great deal of work to perform on the front-end as well. It can be argued that most of your users will think of the interface as the application.

Wheels is here to take you to greener pastures with its form helper functions. Let's get visual with some code examples.

Simple Example: The Old Way

Here is a simple form for editing a user profile. Normally, you would code your web form similarly to this:

Then you would write a script for the form that validates the data submitted, handles interactions with the data source(s), and displays the form with errors that may happen as a result of user input. (And most of that code isn't even included in this example.)

We know that you are quite familiar with the drudgery of typing this sort of code over and over again. Let's not even mention the pain associated with debugging it or adding new fields and business logic!

Making Life Easier: Wheels Form Helpers

The good news is that Wheels simplifies this quite a bit for you. At first, it looks a little different using these conventions. But you'll quickly see how it all ties together and saves you some serious time.

Rewriting the Form with Wheels Conventions

Let's rewrite and then explain.

I know what you are thinking. 9 lines of code can't replace all that work, right? In fact, they do. The HTML output will be very nearly the same as the previous example. By using Wheels conventions, you are saving yourself a lot of key strokes and a great deal of time.

Linking up the Form's Action with startFormTag

As we said, when linking a form to a route, there are 3 pieces of information that you will need to work with:

  1. Route name

  2. Parameters that the route may expect

  3. Request method that the route requires

Use Routes for Form Posts

Most of the time, you'll probably be working with a resource. Your app/config/routes.cfm may look something like this:

If you click the Routes link in the debug footer, you'll be most interested in these types of routes for your forms:

Once you get to this list of routes, it really doesn't matter how you authored them in your app/config/routes.cfm. What matters is that you know the names, methods, and parameters that the routes expect. (With some practice, you'll probably be able to look at app/config/routes.cfm and know exactly what the names, methods, and parameters are though.)

If you need to send the form via another HTTP method, you can pass that in for the method argument as listed in your routes:

Notice above that the user route expects a key parameter, so that is passed into startFormTag as the keyargument.

To drive the point home about routing parameters, let's say that we have this route:

As you can see, the parameters can be anything, not just primary keys.

You would link up the form like so:

A Note About PATCH and DELETE Requests

Browsers (even the modern ones) tend to only work well with GET and POST requests, so how does Wheels also enable PATCH and DELETE requests?

To keep things secure, Wheels will still use method="post" on the form to send PATCH and DELETE requests. But the Wheels router will recognize a PATCH or DELETE request if a form variable called _method is also sent, specifying the PATCH or DELETE method.

So the <form> tag generated along with a method of patch will look something like this:

Refactoring Common Settings with Global Defaults

Here are the settings that you would apply in app/config/settings.cfm:

And here's how our example code can be simplified as a result:

All that the controller needs to provide at this point is a model object instance named profile that contains firstName, lastName, and departmentId properties and a query object named departments that contains identifier and text values. Note that the instance variable is named profile, though the model itself doesn't necessarily need to be named profile.

Refactoring Label Names

Because we've named firstName, lastName, and departmentId in conventional ways (camel case), Wheels will generate the labels for us automatically:

You'll notice that Wheels is even smart enough to translate the departmentId property to Department.

Form Error Messages

If you really want to secure a form, you need to do it server side. Sure, you can add JavaScript here and there to validate your web form. Unfortunately, disabling JavaScript (and thus your JavaScript-powered form validation) is simple in web browsers, and malicious bots tend not to listen to JavaScript.

Displaying a List of Model Validation Errors

Wheels provides you with a tool set of Helper Functions just for displaying error messages as well.

The update action may look something like this:

Let's take the previous form example and add some visual indication to the user about what he did wrong and where, by simply adding the following code on your form page.

How about that? With just that line of code (and the required validations on your object model), Wheels will do the following:

  • Generate an HTML unordered list with a HTML class name of errorMessages.

  • Display all the error messages on your profile object as list items in that unordered list.

  • Wrap each of the erroneous fields in your form with a surrounding <div class="fieldWithErrors"> HTML tag for you to enrich with your ninja CSS skills.

There is no longer the need to manually code error logic in your form markup.

Showing Individual Fields' Error Messages

Let's say that would rather display the error messages just below the failed fields (or anywhere else, for that matter). Wheels has that covered too. All that it takes is a simple line of code for each form field that could end up displaying feedback to the user.

Let's add some error message handlers for the firstName, lastName, and departmentId fields:

And the error messages won't even display if there aren't any. That way you can yet again use the same form code for error and non-error scenarios alike.

Types of Form Helpers

There is a Wheels form helper for basically every type of form element available in HTML. And they all have the ability to be bound to Wheels model instances to make displaying values and errors easier. Here is a brief description of each helper.

Text, Password, and TextArea Fields

Text and password fields work similarly to each other. They allow you to show labels and bind to model object instances to determine whether or not a value should be pre-populated.

May yield the equivalent to this HTML (if we assume the global defaults defined above in the section named Factoring out Common Settings with Global Defaults):

Hidden Fields

Would yield this type of markup:

Select Fields

Take a look at this line:

Assume that the departments variable passed to the options argument contains a query, struct, or array of department data that should be selectable in the drop-down.

Each data type has its advantages and disadvantages:

  • Structs allow you to build out static or dynamic values using whatever data that you please, but there is no guarantee that your CFML engine will honor the order in which you add the elements.

  • Arrays also allow you to build out static or dynamic values, and there is a guarantee that your CFML engine will honor the order. But arrays are a tad more verbose to work with.

Wheels will examine the data passed to options and intelligently pick out elements to populate for the <option>s' values and text.

  • Query: Wheels will try to pick out the first numeric column for value and the first non-numeric column for the display text. The order of the columns is determined how you have them defined in your database.

  • Struct: Wheels will use the keys as the value and the values as the display text.

  • Array: Wheels will react depending on how many dimensions there are. If it's only a single dimension, it will populate both the value and display text with the elements. When it's a 2D array, Wheels will use each item's first element as the value and each element's second element as the display text. For anything larger than 2 dimensions, Wheels only uses the first 2 sub-elements and ignores the rest.

Here's an example of how you might use each option:

When sending a query, if you need to populate your <option>s' values and display text with specific columns, you should pass the names of the columns to use as the textField and valueField arguments.

You can also include a blank option by passing true or the desired text to the includeBlank argument.

Here's a full usage with this new knowledge:

Radio Buttons

Here is an example using a query object called eyeColor to power the possible values:

If profile.eyeColorId's value were already set to 1, the rendered HTML would appear similar to this:

Check Boxes

Note that binding check boxes to model objects is best suited for properties in your object that have a yes/no or true/false type value.

File Fields

Setting a Default Value on an Object-bound Field

Looking at this form code, it isn't 100% evident how to set an initial value for the fields:

What if we want a random title pre-filled, a certain account type pre-selected, and the check box automatically checked when the form first loads?

The answer lies in the account object that the fields are bound to. Let's say that you always wanted this behavior to happen when the form for a new account loads. You can do something like this in the controller:

Now the initial state of the form will reflect the default values setup on the object in the controller.

Helpers That Aren't Bound to Model Objects

Sometimes you'll want to output a form element that isn't bound to a model object.

There are "tag" versions of all of the form helpers that we've listed in this chapter. As a rule of thumb, add Tag to the end of the function name and use the name and value, checked, and selected arguments instead of the objectName and property arguments that you normally use.

Here is a list of the "tag" helpers for your reference:

Passing Extra Arguments for HTML Attributes

Which would produce this HTML:

When a form helper creates more than one HTML element you can typically pass in extra arguments to be set on that element as well. One common example of this is when you need to set a class for a label element; you can do so by passing in labelClass="class-name". Wheels will detect that your argument starts with "label" and assume it should go on the label element and not the input element (or whatever "main" element the form helper creates). This means you could also pass in labelId="my-id" to set the id on the label for example.

Boolean Attributes

HTML includes many boolean attributes like novalidate, disabled, required, etc.

If you want for a Wheels view helper to render one of these attributes, just pass the name of the attribute as an extra argument, set it to true, and Wheels will include the boolean attribute:

HTML5 data Attributes

data attributes in HTML usually look something like this:

Because ColdFusion arguments cannot contain any hyphens, we have constructed a workaround for you for Wheels view helpers.

Let's say you want a data-ajax-url HTML attribute as depicted above. All you need to do is pass in an argument named dataAjaxUrl, and Wheels will convert that attribute name to the hyphenated version in the HTML output.

As an alternative, you can pass in data_ajax_url instead if you prefer underscores, and it will produce the same result.

Special Form Helpers

Wheels provides a few extra form helpers that make it easier for you to generate accessible fields for dates and/or times. These also bind to properties that are of type DATE, TIMESTAMP, DATETIME, etc.

We won't go over these in detail, but here is a list of the date and time form helpers available:

Displaying Links for Pagination

How to create links to other pages in your paginated data in your views.

Displaying Paginated Links with the paginationLinks Function

Given that you have only fetched one paginated query in your controller, this will output the links for that query using some sensible defaults.

How simple is that?

Arguments Used for Customization

The _name_** Argument**

By default, Wheels will create all links with page as the variable that holds the page numbers. So the HTML code will look something like this:

To change page to something else, you use the name argument like so:

The _windowSize_** Argument**

This controls how many links to show around the current page. If you are currently displaying page 6 and pass in windowSize=3, Wheels will generate links to pages 3, 4, 5, 6, 7, 8, and 9 (three on each side of the current page).

The _alwaysShowAnchors_** Argument**

If you pass in true here, it means that no matter where you currently are in the pagination or how many page numbers exist in total, links to the first and last page will always be visible.

Managing More Than One Paginated Query Per Page

Most of the time, you'll only deal with one paginated query per page. But in those cases where you need to get/show more than one paginated query, you can use the handle argument to tell Wheels which query it is that you are referring to.

This argument has to be passed in to both the findAll() function and the paginationLinks() function. (You assign a handle name in the findAll() function and then request the data for it in paginationLinks().)

Here is an example of using handles:

In the controller...

In the view...

That's all you need to know about showing pagination links to get you started. As always, the best way to learn how the view functions work is to just play around with the arguments and see what HTML is produced.

Creating Custom View Helpers

Clean up your views by moving common functionality into helper functions.

As you probably know already, Wheels gives you a lot of helper functions that you can use in your view pages.

Perhaps what you didn't know was that you can also create your own view helper functions and have Wheels automatically make them available to you. To do this, you store your UDFs (User Defined Functions) in different controller-level helper files.

The app/views/helpers.cfm File

Once a UDF is placed in this file, it will be available for use in all your views.

Alternatively, if you only need a set of functions in a specific controller of your application, you can make them controller-specific. This is done by placing a helpers.cfm file inside the controller's view folder.

So if we wanted a set of helpers to generally only be available for your users controller, you would store the UDFs in this file:

Any functions in that file will now only be included for the view pages of that specific controller.

When not to Use Helper Functions

The helpers.cfm files are only meant to be used for views, hence the placement in the app/views folder.

If you need to share non-view functionality across controllers, then those should be placed in the parent controller file, i.e. app/controllers/Controller.cfc. If you need helper type functionality within a single controller file, you can just add it as a function in that controller and make it private so that it can't be called as an action (and as a reminder to yourself of its general purpose as well).

The same applies to reusable model functionality: use the parent file, app/models/Model.cfc. Private functions within your children models work well here, just like with controllers.

If you need to share a function globally across your entire application, regardless of which MVC layer that will be accessing it, then you can place it in the app/events/functions.cfm file.

The Difference Between Partials and Helpers

Both partials and helpers are there to assist you in keeping programmatic details out of your views as much as possible. Both do the job well, and which one you choose is just a matter of preference.

Generally speaking, it probably makes most sense to use partials when you're generating a lot of HTML and helpers when you're not.

Layouts

Wheels allows you to create layouts so that you don't need to header and footer code on every single view template.

Introduction

As a red-blooded CFML developer, you're used to creating include files like header.cfm and footer.cfm, and then using <cfinclude> on every single page to include them. The popular way to do it looks something like this:

Does that mean that you should <cfinclude> your headers and footers in every view in your Wheels app? Heck no! If the structure of your pages ever changed, you would need to edit every single page on your site to make the fix.

Layouts to the rescue!

Implementing a Layout

In your Wheels installation, layout files are stored either directly in the app/views folder or contained in one of the controllers' view folders. Let's go over how layouts work, starting with the simplest way and then moving on to more complex setups.

Let's say that you want to define one layout to be used by every view in your application. You would accomplish this by editing the default layout. The default layout is the app/views/layout.cfm file. In a fresh install of Wheels, you'll notice that it only contains a few lines of code:

Simple Example

For most purposes, this means that you could write code for your page header before the snippet, and write code for the footer after. Here is a simple example of wrapping your view's content with a header and footer.

As you can see, we just wrote code that wraps every view's content with the layout. Pretty cool, huh?

Use of Variables in the Layout

Just like views in Wheels, any variable declared by your application's controller can be used within your layouts. In addition to that, any variables that you set in view templates are accessible to the layouts as well.

Notice in the above code example that there is a variable called title being output in between the <title> tags. This would require that any controller or view using this particular layout would need to set a variable named title.

To help document this, you can use <cfparam> tags at the top of your layout files. That way any developer using your layout in the future could easily see which variables need to be set by the controller.

Here's an example:

The Default Layout

One layout file that is already created for you is app/views/layout.cfm. Think of it as the default layout to be used by any given controller.

If you're writing code for a controller called press and there is no layout created for press, Wheels will automatically use app/views/layout.cfm as the layout.

If you implement a layout for the press controller, then that layout will be used instead of the default layout.

So, how exactly do you implement a layout meant specifically for just one controller? Well, that's next...

Overriding the Default Layout with a Controller-Specific Layout

Let's pretend that you want to create a layout to be used only in a controller called blog. To do this, you would simply create the layout and save it as app/views/blog/layout.cfm.

As you can see, the convention is to place your layout file together with the other view files for the controller.

Overriding the Default Layout Using the usesLayout() Function

With that code placed in the app/controllers/Blog.cfc file, all actions will now wrap their contents with the bloglayoutone.cfm file instead of the layout.cfm file.

That code tells Wheels to apply the blogLayoutOne layout for any actions in this controller except for the homeaction. In the case of the home action, it will fall back to the default behavior (i.e. using the app/views/blog/layout.cfmfile).

If for some reason you do not want the default behavior to be used when conditions aren't met, you can set the useDefault argument to false.

You can even instruct Wheels to run a specific function that will determine the layout handling.

It's worth repeating here that everything inside the controller's config() function runs only once per application and controller. This is why you can't perform the logic that decides which layout to use directly inside the config()function itself. Instead, you tell Wheels to always run the resolveLayout function on each incoming request.

Overriding the Default Layout at the Action Level

Take a look at this example action, called display:

This assumes that you want for your action to use the layout stored at app/views/blog/visitorlayout.cfm.

The default behavior for the layout argument is to look for the file in the current controller's view folder, so here we're still assuming that the display action is in the blog controller. The .cfm extension will also be added automatically by Wheels, so you don't need to specifically include that part.

If you want Wheels to locate the file in a different folder, you can start the layout argument with a forward slash, /. By doing this, Wheels will know you want to start the search in the app/views folder. Let's say that you're storing all miscellaneous layouts in its own folder, simply called layouts. You would display one of them with the following code:

Using No Layout

Nested/Inherited Layouts

Like many templating languages, Wheels offers the ability to create layout files that can be "inherited" by other layout files. The end goal is to create a "parent" layout that has missing sections intended to be filled in by "child" layouts.

The point I'm trying to make here is that we can use this same functionality to set any type of content for, as an example, sections in our layout files. So, armed with this new knowledge, let's create some nested layout awesomeness.

Say we have a global layout and want to fill out content in it from controller specific layouts, here's how we can do it.

The child layout:

The parent layout:

Layouts for Emails and Partials

Date, Media, and Text Helpers

Wheels includes a plethora of view helpers to help you transform data into a format more easily consumed by your applications' users

Wheels's included view helper functions can help you out in those tricky little tasks that need to be performed in the front-end of your web applications. Although they are called miscellaneous, they are in fact categorized into 3 categories:

  • Date Helpers

  • Media Helpers

  • Text Helpers

Date Helpers

Wheels does a good job at simplifying the not so fun task of date and time transformations.

Let's say that you have a comment section in your application, which shows the title, comment, and date/time of its publication. In the old days, your code would have looked something like this:

That works, but it's pretty tedious. And if you think about it, the date will be formatted in a way that is not that meaningful to the end user.

With that minimal change, you have a prettier presentation for your end users. And most important of all, it didn't require you to do anything fancy in your code.

Media Helpers

Working with media is also a walk in the park with Wheels. Let's jump into a few quick examples.

Style Sheets

This will generate the <link> tag for you with everything needed to include the file at public/stylesheets/main.css.

If you need to include more than one style sheet and change the media type to "print" for another, there are arguments for that as well:

Lastly, you can also link to stylesheets at a different domain or subdomain by specifying the full URL:

JavaScript Files

Like with style sheets, you can also specify lists of JavaScript includes as well as full URLs:

Displaying Images

With this simple call, Wheels will generate the <img> tag for public/images/logo.png and also set the width, height and alt attributes automatically for you (based on image dimensions and the image file name). Wheels will also cache this information for later use in your application.

If you need to override the alt attribute for better accessibility, you can still do that too:

Text Helpers

To illustrate what the text helpers can help you with, let's see a piece of code that includes 2 of the text helpers in a simple search results page.

That code will highlight all occurrences of params.q and will pluralize the word "result" to "results" if the number of records in searchResults is greater than 1. How about them apples? No <cfif> statements, no extra lines, no nothing.

The functions we have shown in this chapter are only the tip of the iceberg when it comes to helper functions. There's plenty more, so don't forget to check out the View Helper Functions API.

Linking Pages

Wheels does some magic to help you link to other pages within your app. Read on to learn why you'll rarely use an <a> tag ever again.

Default Wildcard Linking

When installing Wheels, if you open the file at app/config/routes.cfm, you'll see something like this:

For example, if we had a widgets controller with a new action, we could link to it like this:

That would generally produce this HTML markup:

Linking to Routes

Let's work with a set of sample routes to practice creating links:

With this in place, we can load the webroot of our application and click the "Routes" link in the debugging footer to get a list of our routes. You'll see information presented similarly to this:

(As you become more experienced, you'll be able look at routes.cfm and understand what the names and parameters are. Of course, this Routes functionality is a great tool too.)

If we want to link to the routes named newWidget and widgets, it's fairly simple:

As you can see, you create links by calling a method with the route name passed into the route argument. That will generate these links:

The widget route requires an extra step because it has that [key] parameter in its pattern. You can pass that parameter into linkTo as a named argument:

That will produce this markup:

If you have a route with multiple parameters, you must pass all of the placeholders as arguments:

Linking to Resources

Resources are the encouraged routing pattern in Wheels, and you will likely find yourself using this type of route most often.

Once you setup a resource in app/config/routes.cfm, the key is to inspect the routes generated and get a feel for the names and parameters that are expected.

Consider this sample posts resource:

If we wanted to link to the various pages within that resource, we may write something like this on the index:

The above code would generate markup like this:

A Deep Dive into Linking and Routing

Namespaces

Namespaces will generally add the namespace name to the beginning of the route.

Consider this namespace:

To link to the roles resource, you would prefix it with the namespace name:

However, new and edit routes add the action name to the beginning of the route name:

Nested Resources

You have the ability to nest a resource within a resource like so:

To link to the pages resource, you add the parent resource's singular name first (e.g., the parent website is added, making the route name websitePage):

Linking to a Delete Action

Wheels 2.0 introduced security improvements for actions that change data in your applications (i.e., creating, updating, and deleting database records). Wheels protects these actions by requiring that they happen along with a form POST in the browser.

A common UI pattern is to have a link to delete a record, usually in an admin area. Unfortunately, links can only trigger GET requests, so we need to work around this.

To link to a delete request's required DELETE method, we need to code the link up as a simple form with submit button:

Then it is up to you to style the form and submit button to look like a link or button using CSS (using whatever classes that you prefer in your markup, of course).

By the way, this will work with any request method that you please: post, patch, and put as well as delete.

Extreme Example

Which would generate this HTML (or something like it):

Images and Other Embedded HTML in Link Texts

You can also use your CFML engine's built-in string interpolation to embed other HTML into the link text in a fairly readable manner:

Security Notice

If you decide to opt out of encoding, be careful. Any dynamic data passed in to un-encoded values should be escaped manually using your CFML engine's EncodeForHtml() function.

Adding Additional Attributes Like class, rel, and id

The same goes for any other argument that you pass, including but not limited to id, rel, onclick, etc.

What If I Don't Have URL Rewriting Enabled?

Wheels will still correctly build the link markup:

Linking in a Subfolder Deployment of Wheels

Use the linkTo() Function for Portability

An <a> tag is easy enough, isn't it? Why would we need to use a function to do this mundane task? It turns out that there are some advantages. Here's the deal.

Wheels gives you a good amount of structure for your applications. With this, instead of thinking of URLs in the "old way," we think in terms of what route we're sending the user to.

What's more, Wheels is smart enough to build URLs for you. And it'll do this for you based on your situation with URL rewriting. Are you using URL rewriting in your app? Great. Wheels will build your URLs accordingly. Not fortunate enough to have URL rewriting capabilities in your development or production environments? That's fine too because Wheels will handle that automatically. Are you using Wheels in a subfolder on your site, thus eliminating your ability to use URL rewriting? Wheels handles that for you too.

Oh, and another reason is that it's just plain cool too. ;)

The Wheels router begins with a call to , various methods chained from that, and lastly ends with a call to end().

A route name is set up for reference in your CFML code for building , , and such. To build URLs, you'll use this name along with helpers like , , , and so on.

Please note that . is treated as a special characters in patterns and should generally not be used (one exception being when you are ). If your parameters may have . in their value, please use the long form URL format: /?controller=[controller_name]&action=[action_name]&[parameter_name]=[parameter_value]

If you don't see debugging information at the bottom of the page, see the docs for the showDebugInformation setting in the chapter.

If we have a products table and want to have a section of our application for managing the products, we can set up the routes using the method like this in app/config/routes.cfm:

Standard resources using the method assume that there is a primary key associated with the resource. (Notice the [key] placeholder in the paths listed above in the Strongly Encouraged: Resource Routing section.)

Calling (notice that there's no "s" on the end) then exposes the following routes:

But sometimes you just need to define a single one-off route pattern. For this case, you have a method for each HTTP verb: , , , , and .

If you need to limit the actions that are exposed by and , you can also pass in only or exceptarguments:

See the chapter on for strategies for working with this constraint.

That's what the method is for:

If you're upgrading from 1.x or still prefer this style of routing for your Wheels 2+ application, you can use the method to enable it part of it:

products and sessions are your normal controllers. By adding them to the top of the routes file, Wheels looks for them first. But your catch-all route is more specific than the site root (/), so your catch-all should be listed before the call to .

Verification, through the function, is just a special type of filter that runs before actions. With verifications defined in your controller, you can eliminate the need for wrapping your entire actions in <cfif> blocks checking for the existence and types of variables. You also can limit your actions' scopes to specific request types like post, get, and AJAX requests.

Let's say that you want to make sure that all requests coming to a page that handles form submissions are postrequests. While you can do this with a filter and the function, it is more convenient and DRY to do it with the function.

Note that you have to either do a call or abort the request completely after you've done what you wanted to do inside your handler function. If you don't do anything at all and just let the function exit on its own, Wheels will redirect the user back to the page they came from. In other words, you cannot render any content from inside this function but you can let another function handle that part by redirecting to it.

A very convenient and common use of is when you want to make sure that a variables exists and is of a certain type; otherwise, you would like for your controller to redirect the user to a different page.

Now let's see how using the function within Wheels improves this:

exists solely to validate controller and environment level variables and is not a substitute for in your model.

However, should not be used to make sure that values within the address struct themselves are valid (such as making sure that address.zipCode is correct). Because the address struct will be passed in to the model, the validation will be performed there.

Because the Application.cfc file is in the public folder of your Wheels site and it has the necessary framework initialization like onApplicationStart, onRequestStart, It uses to initiailize the vendor/wheels/Global.cfc, which contains the initialization for controller and global functions like , you may wonder what the best way is to use CFML's onApplicationStart, onRequestStart, etc. functions.

This requires that the is installed. It's an IIS extension from Microsoft that you can download for free.

Download Ionic's . NOTE: the version must be v1.2.16 or later.

Tomcat 8 can be configured using RewriteValve. See for examples.

First follow the ().

We've talked previously about how the controller is responsible for deciding which view files to render to the user. Read the chapter if you need to refresh your memory about that topic.

In the simplest case, your controller action (typically a function inside your controller CFC file) will have a view file associated with it. As explained in the chapter, this file will be included automatically at the end of the controller action code. So if you're running the show action in the blog controller, for example, Wheels will include the app/views/blog/show.cfm file.

Not all controller actions need a corresponding view file. Consider the case where you process a form submission. To make sure it's not possible for the user to refresh the page and cause multiple submissions, you may choose to perform the form processing and then send the user directly to another page using the function.

In this case, you can use the function and specify a different action in the action argument (which will include the view page for that action but not run the controller code for it).

Sometimes it's useful to have a view file that can be called from several controller actions. For these cases, you'll typically call with the template argument.

In addition to this normal code that you'll see in most ColdFusion applications—whether they are made for a framework or not—Wheels also gives you some nice constructs to help keep your code clean. The most important ones of these are , Partials, and Helpers.

By "view helpers" we mean everything listed as such in the , so be aware that global helpers, such as , etc, do not encode the content you pass in. When in doubt, simply test by passing in a string and check the HTML source of the output to see whether Wheels encoded it or not.

encodeURLs: When true, calls EncodeForUrl to encode parameter name and values in .

encodeHtmlTags: When true, calls EncodeForHtml to encode tag content in , etc.

encodeHtmlAttributes: When true, calls EncodeForHtmlAttribute to encode attribute values in , etc.

All individual functions also have their own encode argument (can be set to true / false or attributes) that overrides the global setting. Setting it to attributes will only encode HTML attribute values but leave tag content as is. Note that the attributes option is not available on functions that don't produce any tag content (such as for example), in those cases it's enough to pass in either true or false.

Partials in Wheels act as a wrapper around the good old <cfinclude> tag. By calling or , you can include other view files in a page, just like <cfinclude> would. But at the same time, partials make use of common Wheels features like layouts, caching, model objects, and so on.

Websites often display the same thing on multiple pages. It could be an advertisement area that should be displayed in an entire section of a website or a shopping cart that is displayed while browsing products in a shop. You get the idea. To avoid duplicating code, you can place it in a file (the "partial" in Wheels terms) and include that file using on the pages that need it.

To make it clear that a file is a partial and not a full page, we start the filename with an underscore character. You can place the partial file anywhere in the app/views folder. When locating partials, Wheels will use the same rules as it does for the template argument to . This means that if you save the partial in the current controller's view folder, you reference it simply by its name.

For example, if you wanted to have a partial for a comment in your blog controller, you would save the file at app/views/blog/_comment.cfm and reference it (in and ) with just "comment" as the first argument.

Sometimes it's useful to share partials between controllers though. Perhaps you have a banner ad that should be displayed across several controllers. One common approach then is to save them in a dedicated folder for this at the root of the app/views folder. To reference partials in this folder, in this case named shared, you would then pass in "/shared/banner" to instead.

Now that we know why we should use partials and where to store them, let's make a call to from a view page to have Wheels display a partial's output.

You can pass in data by adding named arguments on the call. Because we use the partial argument to determine what file to include, you can't pass in a variable named partial though. The same goes for the other arguments as well, like layout, spacer, and cache.

If you don't want to load the data from a function with the same name as the partial (perhaps due to it clashing with another function name), you can specify the function to load data from with the dataFunction argument to and .

This will wrap the partial with the code found in app/views/boxes/_blue.cfm. Just like with other layouts, you use to represent the partial's content.

Similar to passing in an object, you can also pass in a query result set to . Here's how that looks:

So far we've only talked about , which is what you use from within your views to include other files. There is another similar function as well: . This one is used from your controller files when you want to render a partial instead of a full page. At first glance, this might not make much sense to do. There is one common usage of this though—AJAX requests.

In this case, it's useful to use a partial to display each comment (using as outlined above) and use the same partial when rendering the result of the AJAX request.

Much better! But Wheels can take this process of avoiding repetition one step further. By placing a call in the config() function of the controller, you can tell Wheels what function to run before any desired action(s).

The question then becomes, "Where do I place the restrictAccess() function so I can call it from any one of my controllers?" The answer is that because all controllers extend Controller.cfc, you should probably put it there. The config() function itself with the call to should remain inside your individual controllers though.

You specify if you want to run the filter function before or after the controller action with the type argument to the function. It defaults to running it before the action.

If you want to get a copy of the content that will be rendered to the browser from an after filter, you can use the function. To set your changes to the response afterward, use the function.

If you need to access your filters on a lower level, you can do so by using the and functions. Typically, you'll want to call to return an array of all the filters set on the current controller, make your desired changes, and save it back using the function.

The CORS spec specifies that you are only allowed either a * wildcard, or a specific URL , i.e - it doesn't in itself allow for wildcard subdomains. However in this scenario Wheels will attempt to match the wildcard and return the full matched domain.

Please refer to the chapter for a complete listing of all the variables you can set and their default values.

But there are 2 exceptions to this (which you can make good use of in your code to have the cache re-created at the right times). If the request is a post request (normally coming from a form submission) or if the Flash (you can read everything about the Flash in the chapter) is not empty, then the cache won't be used. Instead, a new fresh page will be created.

Note that by default, any filters set for the action are being run as normal. This means that if you do authentication in the filter (which is a common technique for sites with content where you have to login first to see it), you can still cache those pages safely using the function.

However, to achieve the fastest possible cache, you can override this default and tell Wheels to cache the HTML and serve that exactly as it is to all subsequent requests without running any filters. To do this, set the static argument on to true. This will cache your content using the cfcache tag behind the scenes. This means that the Wheels framework won't even get involved with the subsequent requests until they expire from the cache again (please note that application events like onSessionStart, onRequestStart, etc. will run though).

In Wheels, this is done by using the cache argument in a call to or . You can pass in cache=true or cache=x where x is the number of minutes you want to cache the partial for.

You can turn off this functionality either by using the reload argument to (or any of the dynamic methods that end up calling behind the scenes) or globally by adding set(cacheQueriesDuringRequest=false) to your configuration files.

Once you do that, Wheels will handle everything else. Obviously, the main things Wheels does is obfuscate the primary key value when using the function and deobfuscate it on the receiving end. Wheels will also obfuscate all other params sent in to as well as any value in a form sent using a get request.

In some circumstances, you will need to obfuscate and deobfuscate values yourself if you link to pages without using the function, for example. In these cases, you can use the obfuscateParam() and deObfuscateParam()functions to do the job for you.

The first helper you'll notice in the Wheels-ified version of the form is . This helper allows you to easily link up the form to the action that it's posting to in a secure way.

You'll need to configure the route and method arguments, depending on the route that you're sending the form to. Also, if the route expects any parameters, you must pass those in as arguments to startFormTag as well. If you haven't already, read up about routes in the chapter.

Wheels's default wildcard controller/action-based URLs will not accept form posts for security reasons. This is due to an attack known as . We strongly recommend configuring to post your forms to.

Name
Method
Pattern
Controller
Action

If you are creating a record, your route is likely setup to accept a POST method. That happens to be the default for , so you don't even need to include the method argument. You can then pass the users route name to the route argument:

Name
Method
Pattern
Controller
Action

Under the hood, will also generate a hidden field called _method that passes the request method along with the form POST.

You'll notice that will also add another hidden field along with POSTed requests called authenticityToken, which helps prevent against .

The moral of the story: takes care of all of this for you. If you for some reason decide to wire up your own custom <form> tag that must POST data, be sure to add your own hidden fields for _method and use the helper to generate the hidden field for the authenticityToken that Wheels will require on the POST.

By setting up global defaults (as explained in the ) for the prependToLabel, append, and labelPlacement arguments, you can make the form code ever simpler across your whole application.

If you pass the form an empty instance named profile (for example, created by , the form will display blank values for all the fields. If you pass it an object created by a finder like or , then the form will display the values provided through the object. This allows for us to potentially use the same view file for both create and update scenarios in our application.

If you look at the previous examples, there is one other bit of configuration that we can clean up: the label arguments passed to and .

If you ever need to override a label, you can do so in the model's initializer using the label argument of the method:

Securing the integrity of your web forms in Wheels on the server side is very easy. Assuming that you have read the chapter on , you can rest assured that your code is a lot more secure now.

In the controller, let's say that this just happened. Your model includes validations that require the presence of both firstName and lastName. The user didn't enter either. So in the controller's update action, it loads the model object, sets the values that the user submitted, sees that there was a validation error after calling , and displays the form view again.

Notice that the view for the edit action is rendered if the profile object's returns false.

Notice the call to the function below the firstName, lastName, and departmentId fields. That's all it takes to display the corresponding error messages of each form control on your form.

Hidden fields are powered by the form helper, and it also works similarly to and .

As hinted in our first example of form helpers, the function builds a <select> list with options. What's really cool about this helper is that it can populate the <option>s with values from a query, struct, or array.

Queries allow you to order your results, but you can only use one column. But this can be overcome using .

Radio buttons via also take objectName and property values, and they accept an argument called tagValue that determines what value should be passed based on what the user selects.

If the profile object already has a value set for eyeColorId, then will make sure that that value is checked on page load.

Note that if you don't specify labelPlacement="after" in your calls to , Wheels will place the labels before the form controls.

Check boxes work similarly to radio buttons, except takes parameters called checkedValue and uncheckedValue to determine whether or not the check box should be checked on load.

Because the concept of check boxes don't tie too well to models (you can select several for the same "property"), we recommend using instead if you want to use check boxes for more values than just true/false. See the Helpers That Aren't Bound to Model Objects section below.

The helper builds a file field form control based on the supplied objectName and property.

In order for your form to pass the correct enctype, you can pass multipart=true to :

A search form that passes the user's query as a variable in the URL called q is a good example. In this example case, you would use the function to produce the <input> tag needed.

Much like Wheels's function, any extra arguments that you pass to form helpers will be passed to the corresponding HTML tag as attributes.

For example, if we wanted to define a class on our starting form tag, we just pass that as an extra argument to :

In the chapter titled , we talked about how to get pages of records from the database (records 11-20, for example). Now we'll show you how to create links to the other pages in your view.

If you have fetched a paginated query in your controller (normally done using and the page argument), all you have to do to get the page links to show up is this:

Simple is good, but sometimes you want a little more control over how the links are displayed. You can control the output of the links in a number of different ways. We'll show you the most important ones here. Please refer to the documentation for all other uses.

By the way, perhaps you noticed how Wheels chose to use that hideous question mark in the URL, despite the fact that you have URL rewriting turned on? Because uses in the background, you can easily get rid of it by creating a custom route. You can read more about this in the chapter.

Helper functions, together with the use of , gives you a way to keep your code nice and DRY, but there are a few things to keep in mind as you work with them.

The call to Simple Example represents the output of your page generated by your view files. Whatever code you put before this snippet will be run before the view. Similarly, whatever code you put after the snippet will be run afterward

There's also a different way to set variables that goes hand in hand with the function that you may prefer. It's the function. We'll dig into how that one works later in this chapter.

However, if you need to override the name of the layout file or its location in the folder structure, you can specify what layout file to use with the function in the controller's init function instead.

The function also accepts except, only, and useDefault arguments for further customization.

Another option for overriding layouts is to use the layout argument of the function.

As you may already know, Wheels's function is the last thing called in your actions. This function is run automatically by Wheels, so most of the time, you won't need to call it explicitly in your code.

Note that setting the layout argument on will override any settings you may have made with the function. This gives you finer-grained control.

If you don't want for a given template to be wrapped by a layout at all, you may want to consider creating the page as a partial. See the chapter about for more information.

Another alternative is to use the function and set the layout argument to false.

You can also create a separate layout that only contains the call to the function in it and reference it as described above in Using a Different Layout. This may end up a little ugly though if you start getting a lot of small identical files like this, but the option is there for you at least.

Lastly, if your view needs to return XML code or other data for JavaScript calls, then you should reference the and functions to see which would be best used by your action.

Wheels's approach involves the use of and the function that we mentioned briefly earlier in this chapter.

The function simply includes another layout file. The common usage for this is to include a parent layout from a child layout.

So what about the function? Well, as you may recall the code in the default layout file in Wheels contains this:

The function accepts a name argument. The reason we don't have to use it above is because it defaults to body. This body variable has been set internally in the Wheels framework code with the use of the function.

Note: If your parent file is one of the default ones named layout.cfm you can actually remove the "layout" string above since the default for is layout anyway. We're just including it here for completeness and to show that you can of course achieve this regardless of what your parent layout file is named.

Similar to above, you can remove "body" because that is the default on the function.

That was a fairly basic example of how you can achieve nested layouts in Wheels to DRY up your code. You can of course expand on this by having entire sections of HTML (like a sub menu for example) be created by the child layouts. Also, as a reminder, don't forget that you can use from inside your layout files as well to further keep things DRY.

Besides having layouts for view pages in Wheels, you can also have them on emails that you send out and partial files that you include. We have chosen to speak about these in their respective chapters though: and .

We also have separate chapters about Wheels form helpers in and creating your own helpers in .

Instead of "April 27, 2009 10:10 pm," it may be more helpful to display "a few minutes ago" or "2 hours ago." This can be accomplished with a Wheels date helper called .

First, to include CSS files in your layout, you can use the function:

Including JavaScript files is just as simple with the helper. This time, files are referenced from the public/javascripts folder.

Wheels's helper also provides some simple, yet powerful functionality:

Wheels's built-in function does all of the heavy lifting involved with linking the different pages of your application together. You'll generally be using within your view code.

As you'll soon realize, the function accepts a whole bunch of arguments. We won't go over all of them here, so don't forget to have a look at the for the complete details.

The call to allows a simple linking structure where we can use the helper to link to a combination of controller and action.

If you're developing a non-trivial Wheels application, you'll quickly grow out of the wildcard-based routing. You'll likely need to link to URLs containing primary keys, URL-friendly slugged titles, and nested subfolders. Now would be a good time to take a deep dive into the chapter and learn the concepts.

When you're using to create links to routes, you need to pay attention to 2 pieces of information: the route name and any parameters that the route requires.

Name
Method
Pattern
Controller
Action

We would see these linkable routes generated related to the posts. (See the chapter on for information about posting forms to the rest of the routes.)

Name
Method
Pattern
Controller
Action

The chapter lists your options for generating URLs that are available in your application. Following is an explanation of how to link to the various types of routes available.

The helper generates a form with submit button. As you can see from the example, you can style the submit button itself by prepending any arguments with input (e.g., inputClass).

If you need even more control, you can code up your own with whatever markup that you like. Just be sure to pass method="delete" to the call to startFormTag.

If we were to use all of the parameters for , our code may look something like this:

If you'd like to use an image as a link to another page, pass the output of to the text argument of and use the encode argument to instruct linkTo to only encode attributes:

Like many of the other Wheels view helpers, any additional arguments that you pass to will be added to the generated <a> tag as attributes.

For example, if you'd like to add a class attribute value of button to your link, here's what the call to would look like:

Wheels will handle linking to pages without URL rewriting for you automatically. Let's pretend that you still have Wheels installed in your site root, but you do not have URL rewriting on. How you write your call will not change:

The same would be true if you had Wheels installed in a subfolder, thus perhaps eliminating your ability to use (depending on what web server you have). The same code above may generate this HTML if you had Wheels installed in a subfolder called foo:

If you see the pattern, this gives your application a good deal of portability. For example, you could later enable URL rewriting or move your application to a different subfolder. As long as you're using to build your links, you won't need to change anything extra to your code in order to accommodate this change.

Lastly, if you later install a that needs to modify link markup, that plugin's hook is the helper.

mapper()
links
forms
linkTo()
startFormTag()
urlFor()
responding with multiple formats
Configuration and Defaults
resources()
resources()
resource()
get()
post()
patch()
put()
delete()
resources()
resource()
Linking Pages
namespace()
wildcard()
root()
verifies()
isPost()
verifies()
redirectTo()
verifies()
verifies()
verifies()
Object Validation
verifies()
Wirebox
model()
URL Rewrite Module
ISAPI Rewrite Filter
http://tonyjunkes.com/blog/a-brief-look-at-the-rewrite-valve-in-tomcat-8/
install instructions on the UrlRewriteFilter website
Apache
IIS
Tomcat
Nginx
Rendering Content
Rendering Content
redirectTo()
renderView()
renderView()
Layouts
API reference
capitalize
humanize
URLFor
linkTo
textAreaTag
linkTo
textAreaTag
imageTag
includePartial()
renderPartial()
includePartial()
renderView()
includePartial()
renderPartial()
includePartial()
includePartial()
includePartial()
includePartial()
renderPartial()
includeContent()
includePartial()
includePartial()
renderPartial()
includePartial()
filters()
filters()
filters()
response()
setResponse()
filterChain()
setFilterChain()
filterChain()
setFilterChain()
https://www.foo.com:8080
Configuration and Defaults
Using the Flash
caches()
caches()
includePartial()
renderPartial()
findAll()
findAll()
linkTo()
linkTo()
linkTo()
/app/views/main/example.cfm
<h1>Über uns</h1>
/app/views/main/example.cfm
<cfprocessingdirective pageEncoding="utf-8">
<h1>Über uns</h1>
app/config/routes.cfm
<cfprocessingdirective pageEncoding="utf-8">
<cfscript>
    mapper()
        .get(name="about", pattern="/über-uns", to="pages##about")
        .root(to="wheels##wheels", method="get")
    .end();
</cfscript>
app/views/profiles/edit.cfm
<cfoutput>

<form action="/profile" method="post">
    <div>
        <label for="firstName">First Name</label>
        <input id="firstName" name="firstName" value="#EncodeForHtml(profile.firstName)#">
    </div>

    <div>
        <label for="lastName">Last Name</label>
        <input id="lastName" name="lastName" value="#EncodeForHtml(profile.lastName)#">
    </div>

    <div>
        <label for="department">Department</label>
        <select id="department" name="departmentId">
            <cfloop query="departments">
                <option
                    value="#EncodeForHtml(departments.id)#"
                    <cfif profile.departmentId eq departments.id>
                        selected
                    </cfif>
                >#EncodeForHtml(departments.name)#</option>
            </cfloop>
        </select>
    </div>

    <div>
        <input type="submit" value="Save Changes">
    </div>
</form>

</cfoutput>
app/views/profiles/edit.cfm
<cfoutput>

#startFormTag(route="profile", method="patch")#
    #textField(
        label="First Name",
        objectName="profile",
        property="firstName",
        prependToLabel="<div>",
        append="</div>",
        labelPlacement="before"
    )#

    #textField(
        label="Last Name",
        objectName="profile",
        property="lastName",
        prependToLabel="<div>",
        append="</div>",
        labelPlacement="before"
    )#

    #select(
        label="Department",
        objectName="profile",
        property="departmentId",
        options=departments,
        prependToLabel="<div>",
        append="</div>",
        labelPlacement="before"
    )#

    <div>
        #submitTag()#
    </div>
#endFormTag()#

</cfoutput>
app/config/routes.cfm
mapper()
    .resources("users")
.end();

users

GET

/users

users

index

users

POST

/users

users

create

user

PATCH

/users/[key]

users

update

user

DELETE

/users/[key]

users

delete

app/views/users/new.cfm
<!--- `method` argument defaults to `post`, so we don't need to pass it in. --->
#startFormTag(route="users")#
#endFormTag()
<!---
    Search forms typically are done via `get` requests. This one is to
    the index. --->
#startFormTag(route="users", method="get")#
    #textFieldTag(type="search", name="q")#
    #submitTag("Search")#
#endFormTag()#

<!--- Update forms typically are sent via `patch` requests. --->
#startFormTag(route="user", key=user.key(), method="patch")#
    #textField(objectName="user", property="firstName")#
    #submitTag()#
#endFormTag()#

<!---
    Delete requests are best done via a form post, even if you're styling the
    form to look like a link.
--->
#startFormTag(
    route="user",
    key=user.key(),
    method="delete",
    class="inline-form"
)#
    #buttonTag(content="Delete User", class="link-button")#
#endFormTag()#

productVariation

PATCH

[language]/products/[productKey]/variations/[key]

variations

update

#startFormTag(
    route="productVariation",
    language="es",
    productKey=product,key(),
    key=variation.key(),
    method="patch"
)#
    <!--- ... --->
#endFormTag()
<form action="/users/1234" method="post">
    <input type="hidden" name="_method" value="patch">
    <input type="hidden" name="authenticityToken" value="cxRbrHAnwpG0Ki9vTYW4yg==">
</form>
app/config/settings.cfm
set(
    functionName="textField",
    prependToLabel="<div>",
    append="</div>",
    labelPlacement="before"
);

set(
    functionName="select",
    prependToLabel="<div>",
    append="</div>",
    labelPlacement="before"
);
app/views/profiles/edit.cfm
<cfoutput>

#startFormTag(route="profile", method="patch")#
    #textField(label="First Name", objectName="profile", property="firstName")#
    #textField(label="Last Name", objectName="profile", property="lastName")#

    #select(
        label="Department",
        objectName="profile",
        property="departmentId",
        options=departments
    )#

    <div>
        #submitTag()#
    </div>
#endFormTag()#

</cfoutput>
app/views/profiles/edit.cfm
<cfoutput>

#startFormTag(route="profile", method="patch")#
    #textField(objectName="profile", property="firstName")#
    #textField(objectName="profile", property="lastName")#

    #select(
        objectName="profile",
        property="departmentId",
        options=departments
    )#

    <div>
        #submitTag()#
    </div>
#endFormTag()#

</cfoutput>
app/models/User.cfc
component extends="Model" {
    function init() {
        property(name="lastName", label="Surname");
    }
}
app/controllers/Profiles.cfc
function update() {
    // In this example, we're loading an existing object based on the user's
    // session.
    profile = model("user").findByKey(session.userId);

    // If everything validated, then send user to success message
    if (profile.update(params.profile)) {
        flashInsert(success="Profile updated.");
    }
    // If there were errors with the form submission, show the form again
    // with errors.
    else {
        flashInsert(error="There was an error with your changes.");
        renderView(action="edit");
    }
}
app/views/profiles/edit.cfm
<cfoutput>

#errorMessagesFor("profile")#

#startFormTag(route="profile", method="patch")#
    #textField(objectName="profile", property="firstName")#
    #textField(objectName="profile", property="lastName")#

    #select(
        objectName="department",
        property="departmentId",
        options=departments
    )#

    <div>
        #submitTag()#
    </div>
#endFormTag()#

</cfoutput>
app/views/profiles/edit.cfm
<cfoutput>

#startFormTag(route="profile", method="patch")#
    #textField(objectName="profile", property="firstName")#
    #errorMessageOn(objectName="profile", property="firstName")#

    #textField(objectName="profile", property="lastName")#
    #errorMessageOn(objectName="profile", property="lastName")#

    #selectTag(
        objectName="profile",
        property="departmentId",
        options=departments
    )#
    #errorMessageOn(objectName="profile", property="departmentId")#

    <div>
        #submitTag()#
    </div>
#endFormTag()#

</cfoutput>
#textField(objectName="user", property="username")#
#passwordField(objectName="user", property="password")#
#textArea(objectName="user", property="biography", rows=5, cols=40)#
<div>
    <label for="user-username">Username</label>
    <input id="user-username" type="text" name="user[username]" value="cfguy">
</div>
<div>
    <label for="user-password">Password</label>
    <input id="user-password" type="password" name="user[password]" value="">
</div>
<div>
    <label for="user-biography">Bio</label>
    <textarea id="user-biography" name="user[biography]">
        CF Guy really is a great guy. He's much nicer than .NET guy.
    </textarea>
</div>
#hiddenField(objectName="user", property="referralSourceId")#
<input type="hidden" name="user[referralSourceId]" value="425">
#select(objectName="user", property="departmentId", options=departments)#
// Query generated in your controller --->
departments = findAll(orderBy="name");

// Hard-coded struct set up in app/events/onapplicationstart.cfm
application.departments["1"] = "Sales";
application.departments["2"] = "Marketing";
application.departments["3"] = "Information Technology";
application.departments["4"] = "Human Resources";

// Array built from query call in model
departments = this.findAll(orderBy="lastName,hq");

departmentsArray = [];

for (department in departments) {
    ArrayAppend(
        departmentsArray,
        [department.id, "#department.name# - #department.hq#"]
    );
}
#select(
    objectName="user",
    property="departmentId",
    options=departments,
    valueField="id",
    textField="departmentName",
    includeBlank="Select a Department"
)#
<fieldset>
    <legend>Eye Color</legend>

    <cfloop query="eyeColor">
        #radioButton(
            label=eyeColor.color,
            objectName="profile",
            property="eyeColorId",
            tagValue=eyeColor.id,
            labelPlacement="after"
        )#<br>
    </cfloop>
</fieldset>
<fieldset>
    <legend>Eye Color</legend>

    <input
        type="radio"
        id="profile-eyeColorId-2"
        name="profile[eyeColorId]"
        value="2"
    >
    <label for="profile-eyeColorId-2">Blue</label><br>

    <input
        type="radio"
        id="profile-eyeColorId-1"
        name="profile[eyeColorId]"
        value="1"
        checked="checked"
    >
    <label for="profile-eyeColorId-1">Brown</label><br>

    <input
        type="radio"
        id="profile-eyeColorId-3"
        name="profile[eyeColorId]"
        value="3"
    >
    <label for="profile-eyeColorId-3">Hazel</label><br>
</fieldset>
#checkBox(
    label="Sign me up for the email newsletter.",
    objectName="customer",
    property="newsletterSubscription",
    labelPlacement="after"
)#
#fileField(label="Photo", objectName="profile", property="photo")#
#startFormTag(route="attachments", multipart=true)#
app/views/accounts/new.cfm
#startFormTag(route="accounts")#
    #textField(objectName="account", property="title")#
    #select(objectName="account", property="accountTypeId")#
    #checkBox(objectName="account", property="subscribedToNewsletter")#
#endFormTag()#
app/controllers/Accounts.cfc
component extends="controllers.Controller" {
    function new() {
        local.defaultAccountType = model("accountType").findOne(
          where="isDefault=1"
        );

        account = model("account").new(
            title=generateRandomTitle(),
            accountTypeId=local.defaultAccountType.key(),
            subscribedToNewsletter=true
        );
    }
}
#textFieldTag(label="Search", name="q", value=params.q)#
#startFormTag(route="posts", class="login-form")#
<form action="/posts" method="post" class="login-form">
#textField(objectName="post", property="title", required=true)#
-> <input type="text" name="post[title]" value="" required>
<input type="submit" value="Submit" data-ajax-url="/contacts/send.js">
#submitTag(
    value="Submit",
    dataAjaxUrl=urlFor(route="contactsSend", format="js")
)#
-> <input type="submit" value="Submit" data-ajax-url="/contacts/send.js">
#paginationLinks()#
<a href="/main/userlisting?page=1">
<a href="/main/userlisting?page=2">
<a href="/main/userlisting?page=3">
#paginationLinks(name="pgnum")#
users = model("user").findAll(handle="userQuery", page=params.page, perPage=25);
blogs = model("blog").findAll(handle="blogQuery", page=params.page, perPage=25);
<ul>
    <cfoutput query="users">
        <li>#users.name#</li>
    </cfoutput>
</ul>

<cfoutput>#paginationLinks(handle="userQuery")#</cfoutput>

<cfoutput query="blog">
    #address#<br />
</cfoutput>

<cfoutput>#paginationLinks(handle="blogQuery")#</cfoutput>
app/views/users/helpers.cfm
<cfinclude template="/includes/header.cfm">

<p>Some page content</p>

<cfinclude template="/includes/footer.cfm">
<html>
    <body>
        <cfoutput>#includeContent()#</cfoutput>
    </body>
</html>
<html>
<head>
<title><cfoutput>#title#</cfoutput></title>
</head>

<body>

<div id="container">
    <div id="navigation">
        <ul>
            <cfoutput>
            <li>#linkTo(text="Home", route="main")#</li>
            <li>#linkTo(text="About Us", route="about")#</li>
            <li>#linkTo(text="Contact Us", route="contact")#</li>
            </cfoutput>
        </ul>
    </div>
    <div id="content">
        <cfoutput>#includeContent()#</cfoutput>
    </div>
</div>

</body>
</html>
<!--- Title is required --->
<cfparam name="title" type="string">

<cfoutput>

<html>
<head>
<title>#title#</title>
</head>

<body>

<!--- View's Content --->
<h1>#title#</h1>
#contentForLayout()#

</body>
</html>

</cfoutput>
function config(){
  usesLayout("blogLayoutOne");
}
function config(){
  usesLayout("blogLayoutOne", except="home");
}
function config(){
  usesLayout(name="blogLayoutOne", except="home", useDefault=false);
}
function config(){
  usesLayout("resolveLayout");
}

function resolveLayout(){
    switch(chapter){
    case "index":
    break;
        return "index_layout";
    case "show":
      return "show_layout";
    break;
  }
}
function display(){
  renderView(layout="visitorLayout");
}
function display(){
  renderView(layout="/layouts/plain");
}
<cfoutput>#includeContent()#</cfoutput>
contentFor(pageTitle="My custom title");

<cfoutput>#includeLayout("layout")#</cfoutput>
<html>
    <head>
        <title><cfoutput>#includeContent("pageTitle")#</cfoutput></title>
    </head>
    <body>
        <cfoutput>#includeContent("body")#</cfoutput>
    </body>
</html>
<cfoutput query="comments">
    <div class="comment">
        <h2>#comments.title#</h2>

        <p class="timestamp">
            #DateFormat(comments.createdAt, "mmmm d, yyyy")#
            #LCase(TimeFormat(comments.createdAt, "h:mm tt"))#</p>
        <p>#comments.comment#</p>
    </div>
</cfoutput>
<cfoutput query="comments">
    <div class="comment">
        <h2>#comments.title#</h2>

        <p class="timestamp">(#timeAgoInWords(comments.createdAt)#)</p>
        <p>#comments.comment#</p>
    </div>
</cfoutput>
<!--- layout.cfm --->
<cfoutput>
    #styleSheetLinkTag("main")#
</cfoutput>
#styleSheetLinkTag(sources="main,blog")#
#styleSheetLinkTag(source="printer", media="print")#
#styleSheetLinkTag(    source="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/cupertino/jquery-ui.css"
)#
#javaScriptIncludeTag("jquery")#
#javaScriptIncludeTag("https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js")#
<cfoutput>
    #imageTag("logo.png")#
</cfoutput>
#imageTag(source="logo.png", alt="ColdFusion on Wheels")#
<!--- Query of search results --->
<cfparam name="searchResults" type="query">

<!--- Search query provided by user --->
<cfparam name="params.q" type="string">

<cfoutput>
    <p>
       #highlight(text="Your search for #params.q#", phrases=params.q)#
       returned #searchResults.RecordCount#
       #pluralize(word="result", count=searchResults.RecordCount)#.
    </p>
</cfoutput>
app/config/routes.cfm
mapper()
    .wildcard()
    .root(method = "get")
.end();
#linkTo(text="New Widget", controller="widgets", action="new")#
<a href="/widgets/new">New Widget</a>
app/config/routes.cfm
mapper()
    .get(name="newWidget", pattern="widgets/new", to="widgets##new")
    .get(name="widget", pattern="widgets/[key]", to="widgets##show")
    .get(name="widgets", to="widgets##index")
    .root(to="wheels##wheels")
.end();

newWidget

GET

/widgets/new

widgets

new

widget

GET

/widgets/[key]

widgets

show

widgets

GET

/widgets

widgets

index

#linkTo(text="All Widgets", route="widgets")#
#linkTo(text="New Widget", route="newWidget")#
<a href="/widgets">All Widgets</a>
<a href="/widgets/new">New Widget</a>
#linkTo(text="The Fifth Widget", route="widget", key=5)#
<a href="/widgets/5">The Fifth Widget</a>
Example
<!--- app/config/routes.cfm --->
<cfscript>
mapper()
    .get(
        name="widgetVariation",
        pattern="widgets/[widgetKey]/variations/[key].[format]",
        to="widgetVariations##show"
    )
.end();
</cfscript>

<!--- View file --->
<cfoutput>
#linkTo(
    text="A fine variation (PDF)",
    route="widgetVariation",
    widgetKey=5,
    key=20
    format="pdf"
)#
</cfoutput>

<!--- HTML generated --->
<a href="/widgets/5/variations/20.pdf">A fine variation (PDF)</a>
app/config/routes.cfm
mapper()
    .resources("posts")
.end();

posts

GET

/posts

posts

index

newPost

GET

/posts/new

posts

new

editPost

GET

/posts/[key]/edit

posts

edit

post

GET

/posts/[key]

posts

show

app/views/posts/index.cfm
<nav class="global-nav">
    #linkTo(text="All Posts", route="posts")#
</nav>

<h1>Posts</h1>
<p>
    #linkTo(text="New Post", route="newPost")#
</p>

<ul>
    <cfloop query="posts">
        <li>
            #linkTo(text=posts.title, route="post", key=posts.id)#
            [#linkTo(text="Edit", route="editPost", key=posts.id)#]
        </li>
    </cfloop>
</ul>
<nav class="global-nav">
    <a href="/posts">All Posts</a>
</nav>

<h1>Posts</h1>
<p>
    <a href="/posts/new">New Post</a>
</p>

<ul>
    <li>
        <a href="/posts/1">Some Title</a>
        [<a href="/posts/1/edit">Edit</a>]
    </li>
</ul>
mapper()
    .namespace("admin")
        .resources("roles")
    .end()
.end();
#linkTo(name="List Roles", route="adminRoles")#

#linkTo(text=role.title, route="adminRole", key=role.key())#
#linkTo(text="New Role", route="newAdminRole")#
#linkTo(text="Edit Role", route="editAdminRole", key=role.key())#
mapper()
    .resources(name="websites", nested=true)
        .resources("pages")
    .end()
.end();
<!---
    Also notice that the parent route's primary key parameter is
    `websiteKey`:
--->
#linkTo(text="All Pages", route="websitePages", websiteKey=website.key())#

#linkTo(text="New Page", route="newWebsitePage", websiteKey=website.key())#

<!--- And the child resource's primary key parameter is `key`: --->
#linkTo(
    text="Show Page",
    route="websitePage",
    websiteKey=website.key(),
    key=page.key()
)#

#linkTo(
    text="Edit Page",
    route="editWebsitePage",
    websiteKey=website.key(),
    key=page.key()
)#
#buttonTo(
    text="Delete",
    route="category",
    key=category.key(),
    method="delete",
    inputClass="button-as-link"
)#
#linkTo(
    text='<i class="rock-fist"></i> Wheels Rocks!',
    route="wheelsRocks",
    key=55,
    params="rocks=yes&referral=wheels.dev",
    anchor="rockin",
    host="www.example.co.uk",
    protocol="https",
    onlyPath=false,
    encode="attributes"
)#
<a href="https://www.example.co.uk/wheels/rocks/55?rocks=yes&amp;amp;referral=wheels.dev#rockin"><i class="rock-fist"></i> Wheels Rocks!</a>
#linkTo(
    text=imageTag(source="authors.jpg"),
    route="blogAuthors",
    encode="attributes"
)#
#linkTo(
    text='<i class="fa fa-user"></i> #EncodeForHtml(employees.fullName)#',
    route="employee",
    key=employees.id,
    encode="attributes"
)#
#linkTo(text="Check Out", route="checkout", class="button")#
#linkTo(
    text="This link isn't as pretty, but it still works",
    route="product",
    key=product.key()
)#
<a href="/index.cfm/products/3">This link isn't as pretty, but it still works</a>
<a
    href="/foo/index.cfm?route=product&amp;key=3">
    This link isn't as pretty, but it still works
</a>
startFormTag()
Routing
Cross Site Request Forgery (CSRF)
routes
startFormTag()
startFormTag()
startFormTag()
Cross-Site Request Forgery (CSRF) attacks
startFormTag()
authenticityTokenField()
Configuration and Defaults
new()
findOne()
findByKey()
textField()
select()
property()
Object Validation
update()
update()
errorMessageOn()
hiddenField()
textField()
passwordField()
select()
Calculated Properties
radioButton()
radioButton()
radioButton()
checkBox()
checkBoxTag()
fileField()
startFormTag()
textFieldTag()
checkBoxTag()
hiddenFieldTag()
passwordFieldTag()
radioButtonTag()
selectTag()
textAreaTag()
textFieldTag()
linkTo()
startFormTag()
dateSelect()
dateSelectTags()
timeSelect()
timeSelectTags()
dateTimeSelect()
dateTimeSelectTags()
yearSelectTag()
monthSelectTag()
daySelectTag()
hourSelectTag()
minuteSelectTag()
secondSelectTag()
Getting Paginated Data
findAll()
paginationLinks()
paginationLinks()
linkTo()
Routing
Partials
includeContent()
includeContent()
contentFor()
usesLayout()
usesLayout()
renderView()
renderView()
renderView()
usesLayout()
Partials
renderView()
includeContent()
renderNothing()
renderText()
includeLayout()
contentFor()
includeLayout()
contentFor()
includeContent()
contentFor()
includeLayout()
includeContent()
includePartial()
Sending Email
Partials
Form Helpers and Showing Errors
Creating Custom View Helpers
timeAgoInWords()
styleSheetLinkTag()
javaScriptIncludeTag()
imageTag()
linkTo()
linkTo()
linkTo()
documentation
wildcard()
linkTo()
Routing
linkTo()
Form Helpers and Showing Errors
Routing
buttonTo()
startFormTag()
linkTo()
imageTag()
linkTo()
linkTo()
linkTo()
linkTo()
URL Rewriting
linkTo()
linkTo()
plugin
linkTo()

Migrations in Production

Techniques for migrating your database in production

Once you've created your migration files and committed your changes (you are all using source control - right?) you might be wondering about the different ways to migrate your database in a production environment.

Manual Migrations via GUI

Probably one of the most common and basic deployment types is a standalone virtual machine, running ACF or Lucee, with a database server such as MySQL running on the same box. In this scenario, we could probably stick with the simplest option: there is after all, probably only one instance of the site running.

  • Put the site into maintenance mode (this is always good practice when deploying new code)

  • Load the internal Migration GUI, migrate your database. Note: Ensure your IP address is in the maintenance mode exclusion list: the debug footer may not be available, so make a note of the url string ?controller=wheels&action=wheels&view=migrate

  • Reload the application back into production mode

Automatic Migrations

You may well have a more complicated setup, such as being behind a load balancer, or having dynamic instances of your application - such as AWS ElasticBeanstalk - where logging into the same instance isn't practical; it may be your application is an API where a request could get routed to any node in the cluster, or that "sticky" sessions aren't enabled.

This means running the migrations manually via GUI isn't a practical option - you might accidentally leave a node in the cluster in maintenance mode and not be able to easily return to it etc.

In this scenario, you could use the built-in autoMigrateDatabase setting: this will automatically migrate the database to the latest schema version when the application starts.

This would fire for each node on a cluster and would fire on each application restart - however, the overhead would be minimal (one additional database call).

To activate this feature, just use set(autoMigrateDatabase=true) in your app/config/production/settings.cfm settings, to ensure it only fires in production mode.

Programmatic Migrations

It might be that full automatic migrations aren't necessary, or undesirable for some reason. You could have a script which essentially replaces the GUI functions and call the migration methods manually.

Please consult the internal documentation API reference under Configurations > Database Migrations for details of the various functions available to you.

Further considerations with automatic migrations

If you are using automatic migrations, then you could lock down production mode even further. With Wheels 2.x there is more data available to development mode, such as the internal documentation, routing GUI and Migration GUI.

Turn off environment switching

You can force Wheels to remain in production via set(allowEnvironmentSwitchViaUrl=false) - this will disable ?reload=maintenance style URLs where there is a configuration change, but simple reloading such as ?reload=true will still work. This setting should be approached with caution, as once you've entered into a mode with this setting on, you can't then switch out of it.

Creating Records

How to create new objects and save them to the database.

newAuthor = model("author").new();

We now have an empty Author object that we can start filling in properties for. These properties correspond with the columns in the authors database table, unless you have mapped them specifically to columns with other names (or mapped to an entirely different table).

newAuthor.firstName = "John";
newAuthor.lastName = "Doe";
newAuthor.save();

Creating Based on a Struct

Given that params.newAuthor is a struct containing the firstName and lastName variables, the code below does the same as the code above (without saving it though).

newAuthor = model("author").new(params.newAuthor);

Saving Straight to the Database

model("author").create(params.newAuthor);

The Primary Key

Note that if we have opted to have the database create the primary key for us (which is usually done by auto-incrementing it), it will be available automatically after the object has been saved.

This means you can read the value by doing something like this. (This example assumes you have an auto-incrementing integer column named id as the primary key.)

<cfscript>
newAuthor = model("author").new();
newAuthor.firstName = "Joe";
newAuthor.lastName = "Jones";
newAuthor.save();
</cfscript>
<cfoutput>#newAuthor.id#</cfoutput>

Don't forget that you can name your primary key whatever you want, and you can even use composite keys, natural keys, non auto-incrementing, and so on.

No matter which method you prefer, Wheels will use database introspection to see how your table is structured and act accordingly.

Using Database Defaults

The best way of handling model defaults is usually by setting a default constraint in your database. When Wheels saves the model to the database, it will automatically insert the default value if you haven't provided one within your model.

However, unlike the primary key, Wheels will not automatically load database defaults after saving as it requires an additional database call and in most cases is not required. (After saving, the most common action is to redirect, in which case you would reload the newly saved model in the next request anyway.)

newAuthor = model("author").new();
newAuthor.firstName = "Joe";
newAuthor.lastName = "Jones";
newAuthor.save(reload=true);

Using Model Defaults

property(name="welcomeText", defaultValue="Hello world!");

This is effectively the same as doing this:

model("myModel").new(welcomeText="Hello world!");

..except you only need to set it once per model.

Object Relational Mapping

An overview of Object Relational Mapping (ORM) and how is it used in Wheels. Learn how ORM simplifies your database interaction code.

Mapping objects in your application to records in your database tables is a key concept in Wheels. Let's take a look at exactly how this mapping is performed.

Class and Object Methods

Unlike most other languages, there is no notion of class level (a.k.a. "static") methods in CFML. This means that even if you call a method that does not need to use any instance data, you still have to create an object first.

In Wheels, we create an object like this:

model("author");

Obviously, author is just an example here, and you'll use the names of the .cfc files you have created in the app/models folder.

authorClass = model("author");
authorObject = authorClass.findByKey(1);

For readability, this is usually combined into the following:

authorObject = model("author").findByKey(1);
authorObject.update(firstName="Joe");

In this case, the above code updates firstName field of the author record with a primary key value of 1 to Joe.

Primary Keys

Traditionally in Wheels, a primary key is usually named id, it increments automatically, and it's of the integer data type. However, Wheels is very flexible in this area. You can setup your primary keys in practically any way you want to. You can use natural keys (varchar, for example), composite keys (having multiple columns as primary keys), and you can name the key(s) whatever you want.

You can also choose whether the database creates the key for you (using auto-incrementation, for example) or create them yourself directly in your code.

What's best, Wheels will introspect the database to see what choices you have made and act accordingly.

Tables and Classes

Wheels comes with a custom built ORM. ORM stands for "Object-Relational Mapping" and means that tables in your relational database map to classes in your application. The records in your tables map to objects of your classes, and the columns in these tables map to properties on the objects.

To create a class in your application that maps to a table in your database, all you need to do is create a new class file in your app/models folder and make it extend the Model.cfc file.

component extends="Model" {
}

If you don't intend to create any custom methods in your class, you can actually skip this step and just call methods without having a file created. It will work just as well. As your application grows, you'll probably want to have your own methods though, so remember the app/models folder. That's where they'll go.

Once you have created the file (or deliberately chosen not to for now), you will have a bunch of methods available handle reading and writing to the authors table. (For the purpose of showing some examples, we will assume that you have created a file named Author.cfc, which will then be mapped to the authors table in the database).

For example, you can write the following code to get the author with the primary key of 1, change his first name, and save the record back to the database.

auth = model("author").findByKey(1);
auth.firstName = "Joe";
auth.save();

Table and CFC Naming

By default, a table name should be the plural version of the class name. So if you have an Author.cfc class, the table name should be authors.

So, for example, if you wanted for your author model to map to a table in your database named tbl_authors, you would add the following code to the config() method:

component extends="Model" {
    function config() {
    table("tbl_authors");
  }
}

Models Without Database Tables

Most of the time, you will want to have your model mapped to a database table. However, it is possible to skip this requirement with a simple setting:

function config() {
    table(false);
}

Features supported:

  • Properties

  • Validations

  • Callbacks involving initialisation and validations

Columns and Properties

Objects in Wheels have properties that correspond to the columns in the table that it maps to. The first time you call a method on a model, Wheels will reflect on the schema inside the database for the table the class maps to and extract all the column information.

Note about database permissions

In order for Wheels to successfully read all schema data from your database be sure the data source user has the required access for your DBMS. For example, Microsoft SQL Server requires the "ddl_admin" permission for some meta data such as column defaults.

To keep things as simple as possible, there are no getters or setters in Wheels. Instead, all the properties are made available in the this scope.

component extends="Model" {
    function config() {
    property(name="firstName", column="tbl_auth_f_name");
  }
}

Blank Strings and NULL Values

Since there is no concept of null / nil in CFML, Wheels will assume that when you save a blank string to the database it should be converted to NULL.

For this reason we recommend that you avoid having blank strings stored in the database (since there is no way to distinguish them from NULL values once they've been mapped to a Wheels object / result set).

Updating Records

Updating records in your database tables.

A Practical Example

Let's start with an example of getting a blog post from the database, updating its title, and saving it back:

post = model("post").findByKey(33);
post.title = "New version of Wheels just released";
post.save();

You can also change the values of one or more properties and save them to the database in one single call using the update() method, like this:

post = model("post").findByKey(33);
post.update(title="New version of Wheels just released");

Updating Via struct Values

You can also pass in name/value pairs to update() as a struct. The main reason this method accepts a struct is to allow you to easily use it with forms.

This is how it would look if you wanted to update the properties for a post based on a submitted form.

post = model("post").findByKey(params.key);
post.update(params.post);

It's also possible to combine named arguments with a struct, but then you need to name the struct argument as properties.

Example:

post = model("post").findByKey(params.key);
post.update(title="New version of Wheels just released", properties=params.post);

Combine Reading and Updating into a Single Call

The updateByKey() Method

This method returns the object with the primary key value you specified. If the object does not pass validation, it will be returned anyway, but nothing will be saved to the database.

result = model("post").updateByKey(33, params.post);
result = model("post").updateByKey(id=33, title="New version of Wheels just released", published=1);

Updating Multiple Rows with updateAll()

The where argument is used exactly as you specify it in the WHERE clause of the query (with the exception that Wheels automatically wraps everything properly in cfqueryparam tags). So make sure that you place those commas and quotes correctly!

An example:

recordsReturned = model("post").updateAll(
        published=1, publishedAt=Now(), where="published=0"
);

Deleting Records

Deleting records from your database tables.

Delete Callbacks

If you have callbacks however, this is what happens:

If these return true, Wheels will proceed and delete the record from the table. If false is returned from the callback code, processing will return to your code without the record being deleted. (false is returned to you in this case.)

Example of Deleting a Record

Here's a simple example of fetching a record from the database and then deleting it.

aPost = model("post").findByKey(33);
aPost.delete();

Getting Paginated Data

Improve database performance and simplify your user interface by using pagination.

If you searched for "coldfusion" on Google, would you want all results to be returned on one page? Probably not because it would take a long time for Google to first get the records out of its index and then prepare the page for you. Your browser would slow to a halt as it tried to render the page. When the page would finally show up, it would be a pain to scroll through all those results.

Rightly so, Google uses pagination to spread out the results on several pages.

And in Wheels, it's really simple to do this type of pagination. Here's how:

  • Get records from the database based on a page number. Going back to the Google example, this would mean getting records 11-20 when the user is viewing the second results page. This is (mostly) done using the findAll() function and the page and perPage arguments.

Learning by Example

Let's jump straight to an example:

authors = model("Author").findAll(page=2, perPage=25, order="lastName");

That simple code will return authors 26-50 from the database, ordered by their last name.

What SQL statements are actually being executed depends on which database engine you use. (The MySQL adapter will use LIMIT and OFFSET, and the Microsoft SQL Server adapter will use TOP and some tricky sub queries.) Turn on debugging in the ColdFusion Administrator if you want to see exactly what's going on under the hood.

One important thing that you should be aware of is that pagination is done based on objects and not records. To illustrate what that means, we can expand on the above example a little:

authorsAndBooks = model("Author").findAll(
  include="Books", page=2, perPage=25, order="lastName"
);

Here, we tell Wheels that we also want to include any books written by the authors in the result. Since it's possible that an author has written many books, we can't know in advance how many records we'll get back (as opposed to the first example, where we know we will get 25 records back). If each author has written 2 books, for example, we will get 50 records back.

If you do want to paginate based on the books instead, all that you need to do is flip the findAll() statement around a little:

booksAndAuthors = model("Book").findAll(
  include="Author", page=2, perPage=25, order="lastName"
);

Here, we call the findAll() function on the Book class instead, and thereby we ensure that the pagination is based on the books and not the authors. In this case, we will always get 25 records back.

If you need to know more about the returned query, you can use the pagination() function which returns a struct with keys pagination().currentPage, pagination().totalPages and pagination().totalRecords.

That's all there is to it, really. The best way to learn pagination is to play around with it with debugging turned on.

Column Statistics

Use Wheels to get statistics on the values in a column, like row counts, averages, highest values, lowest values, and sums.

Since Wheels simplifies so much for you when you select, insert, update, and delete rows from the database, it would be a little annoying if you had to revert back to using cfquery and COUNT(id) AS x type queries when you wanted to get aggregate values, right?

Counting Rows

To count how many rows you have in your authors table, simply do this:

authorCount = model("author").count();
authorCount = model("author").count(where="lastName LIKE 'A%'");

Simple enough. But what if you wanted to count only authors in the USA, and that information is stored in a different table? Let's say you have stored country information in a table called profiles and also setup a hasOne / belongsTo association between the author and profile models.

In our case, the code would end up looking something like this:

authorCount = model("author").count(include="profile", where="countryId=1 AND lastName LIKE 'A%'");

Or, if you care more about readability than performance, why not just join in the countries table as well?

authorCount = model("author").count(include="profile(country)", where="name='USA' AND lastName LIKE 'A%'");

In the background, these functions all perform SQL that looks like this:

MySQL
SELECT COUNT(*)
FROM authors
WHERE ...

However, if you include a hasMany association, Wheels will be smart enough to add the DISTINCT keyword to the SQL. This makes sure that you're only counting unique rows.

For example, the following method call:

authorCount = model("author").count(include="books", where="title LIKE 'Wheels%'");

Will execute this SQL (presuming id is the primary key of the authors table and the correct associations have been setup):

MySQL
SELECT COUNT(DISTINCT authors.id)
FROM authors LEFT OUTER JOIN books ON authors.id = books.authorid
WHERE ..

Getting an Average

The same goes for the remaining column statistics functions as well; they all accept the property argument.

Here's an example of getting the average salary in a specific department:

avgSalary = model("employee").average(property="salary", where="departmentId=1");

You can also pass in distinct=true to this function if you want to include only each unique instance of a value in the average calculation.

Getting the Highest and Lowest Values

They are pretty self explanatory, as you can tell by the following examples:

highestSalary = model("employee").maximum("salary");
lowestSalary = model("employee").minimum("salary");

Getting the Sum of All Values

Let's wrap up this chapter on a happy note by getting the total dollar amount you've made:

howRichAmI = model("invoice").sum("billedAmount");

Grouping Your Results

All of the methods we've covered in this chapter accepts the group argument. Let's build on the example with getting the average salary for a department above, but this time, let's get the average for all departments instead.

avgSalaries = model("employee").average(property="salary", group="departmentId");

When you choose to group results like this you get a cfquery result set back, as opposed to a single value.

Limited Support

The group argument is currently only supported on SQL Server and MySQL databases.

Dynamic Finders

Make your model calls more readable by using dynamic finders.

Since the introduction of onMissingMethod() in CFML, we have been able to port over the concept of _dynamic finders_from Rails to Wheels.

The concept is simple. Instead of using arguments to tell Wheels what you want to do, you can use a dynamically-named method.

For example, the following code:

me = model("user").findOne(where="email='me@myself.com'");

Can also be written as:

me = model("user").findOneByEmail("me@myself.com");

Through the power of onMissingMethod(), Wheels will parse the method name and figure out that the value supplied is supposed to be matched against the email column.

Dynamic Finders Involving More than One Column

You can take this one step further by using code such as:

me = model("user").findOneByUserNameAndPassword("bob,pass");

In this case, Wheels will split the function name on the And part and determine that you want to find the record where the username column is "bob" and the password column is "pass".

When you are passing in two values, make special note of the fact that they should be passed in as a list to one argument and not as two separate arguments.

Works with findAll() too

Passing in Other Finder Parameters

The below code, for example, is perfectly valid:

users = model("user").findAllByState(value="NY", order="name", page=3);

When passing in multiple arguments like above, you have to start naming them instead of relying on the order of the arguments though. When doing so, you need to name the argument value if you're passing in just one value and values if you're passing in multiple values in a list. In other words, you need to name it values when calling an Andtype dynamic finder.

users = model("user").findAllByCityAndState(
        values="Buffalo,NY", order="name", page=3
);

Avoid the Word "And" in Database Column Names

Keep in mind that this dynamic method calling will break down completely if you ever name a column firstandlastname or something similar because Wheels will then split the method name incorrectly. So avoid using "And" in the column name if you plan on taking advantage of dynamically-named finder methods.

Reading Records

Returning records from your database tables as objects or queries.

Fetching a Row by Primary Key Value

If the record exists, it is returned to you as an object. If not, Wheels will return the boolean value false.

In the following example, we assume that the params.key variable has been created from the URL (for example a URL such as http://localhost/blog/viewauthor/7.)

In your controller:

author = model("author").findByKey(params.key);
if(!IsObject(author)){
    flashInsert(message="Author #params.key# was not found");
  redirectTo(back=true);
}

In your view:

<cfoutput>Hello, #author.firstName# #author.lastName#!</cfoutput>

Fetching a Row by a Value Other Than the Primary Key

Often, you'll find yourself wanting to get a record (or many) based on a criteria other than just the primary key value.

As an example, let's say that you want to get the last order made by a customer. You can achieve this by using the findOne() method like so:

anOrder = model("order").findOne(order="datePurchased DESC");

Fetching Multiple Rows

Arguments for findOne() and findAll()

select Argument

This maps to the SELECT clause of the SQL statement.

Wheels is pretty smart when it comes to figuring out what to select from the database table(s). For example, if nothing is passed in to the select argument, Wheels will assume that you want all columns returned and create a SELECTclause looking something like this:

artists.id,artists.name

As you can see, Wheels knows that the artist model is mapped to the artists table and will prepend the table name to the column names accordingly.

If you have mapped columns to a different property name in your application, Wheels will take this into account as well. The end result then could look like this:

artists.id,artists.fname AS firstName

If you select from more than one table (see the include argument below) and there are ambiguous column names, Wheels will sort this out for you by prepending the model name to the column name.

Let's say you have a column called name in both the artists and albums tables. The SELECT clause will be created like this:

artists.name,albums.name AS albumName

If you use the include argument a lot, you will love this feature as it saves a lot of typing.

If you don't want to return all properties, you can override this behavior by passing in a list of the properties you want returned.

If you want to take full control over the SELECT clause, you can do so by specifying the table names (i.e. author.firstName) in the select argument or by using alias names (i.e., firstname AS firstName). If Wheels comes across the use of any of these techniques, it will assume you know what you're doing and pass on the select argument straight to the SELECT clause with no changes.

A tip is to turn on debugging when you're learning Wheels so you can get a good understanding of how Wheels creates the SQL statements.

where Argument

This maps to the WHERE clause of the SQL statement. Wheels will also convert all your input to cfqueryparam tags for you automatically.

There are some limitations to what you can use in the where argument, but the following SQL will work: =, !=, <>, <, <=, >, >=, LIKE, NOT LIKE, IN, NOT IN, IS NULL, IS NOT NULL, AND, and OR. (Note that it's a requirement to write SQL keywords in upper case.) In addition to this, you can use parentheses to group conditional SQL statements together.

It's worth mentioning that although Wheels does not support the BETWEEN operator, you can get around this by using >= and <=.

Example with numeric value:

items = model("item").findAll(where="price >= 100 AND price <= 500");

The same goes for NOT BETWEEN:

items = model("item").findAll(where="price <= 100 OR price >= 500");

In Wheels ORM queries, you need to surround strings with single quotes or leave the quotes out if you're passing in a number or boolean.

Example with non-numeric value:

bobsArticles = model("author").findAll(where="firstName='Bob'");

order Argument

This maps to the ORDER clause of the SQL statement. If you don't specify an order at all, none will be used. (Makes sense, eh?) So in those cases, the database engine will decide in what order to return the records. Note that it's a requirement to write the SQL keywords ASC and DESC in upper case.

There is one exception to this. If you paginate the records (by passing in the page argument) without specifying the order, Wheels will order the results by the primary key column. This is because pagination relies on having unique records to order by.

include Argument

This is a powerful feature that you can use if you have set up associations in your models.

If, for example, you have specified that one Author has many Articles, then you can return all authors and articles in the same call by doing this:

bobsArticles = model("author").findAll(where="firstName='Bob'", include="Articles");

maxRows Argument

page and perPage Arguments

Set these if you want to get paginated data back.

So if you wanted records 11-20, for example, you write this code:

bobsArticles = model("author").findAll(
        where="firstName='Bob'", include="Articles", page=2, perPage=10
);

cache Argument

This is the number of minutes to cache the query for. This is eventually passed on to the cachedwithin attribute of the cfquery tag.

returnAs Argument

In the beginning of this chapter, we said that you either get a query or an object back depending on the method that you call. But you can actually specify the return type so that you get either an object, a query, or an array of objects back.

users = model("user").findAll(returnAs="objects");

We recommend sticking to this convention as much as possible because of the CFML engines' slow CreateObject() function. Be careful when setting returnAs to objects. You won't want to create a lot of objects in your array and slow down your application unless you absolutely need to.

useIndex Argument

If you have a specific index setup on a table that you'd like the findAll() call to use, you can specify a structure of arguments for each model/index you'd like to use. Only MySQL and SQLServer support index hints.

model("author").findAll(
    include="posts",
    useIndex={
        author="idx_authors_123",
        post="idx_posts_123"
		}
);

Ignoring Column(s)

For the above finder methods you are able to ignore specified column(s). For instance this can be useful when working with an existing database that has many irrelevant columns. Ignoring column(s) is achieved by adjusting the desired model's config function like below.

Ignoring one column:

component extends="Model" {
		function config() {
			ignoredColumns(columns = ["column1"]);
		}
}

Ignoring multiple columns:

component extends="Model" {
		function config() {
			ignoredColumns(columns = ["column1","column2"]);
		}
}

Associations

Through some simple configuration, Wheels allows you to unlock some powerful functionality to use your database tables' relationships in your code.

Associations in Wheels allow you to define the relationships between your database tables. After configuring these relationships, doing pesky table joins becomes a trivial task. And like all other ORM functions in Wheels, this is done without writing a single line of SQL.

3 Types of Associations

In order to set up associations, you only need to remember 3 simple methods. Considering that the human brain only reliably remembers up to 7 items, we've left you with a lot of extra space. You're welcome. :)

The association methods should always be called in the config() method of a model that relates to another model within your application.

The belongsTo Association

If we had a comments table that contains a foreign key to the posts table called postid, then we would have this config() method within our comment model:

app/models/comment.cfc
component extends="Model" {

    function config() {
        belongsTo("post");
    }

}

The hasOne and hasMany Associations

On the other side of the relationship are the "has" functions. As you may have astutely guessed, these functions should be used according to the nature of the model relationship.

At this time, you need to be a little eccentric and talk to yourself. Your association should make sense in plain English language.

An example of hasMany

So let's consider the post / comment relationship mentioned above for belongsTo(). If we were to talk to ourselves, we would say, "A post has many comments." And that's how you should construct your post model:

app/models/Post.cfc
component extends="Model" {

    function config() {
        hasMany("comments");
    }

}

You may be a little concerned because our model is called comment and not comments. No need to worry: Wheels understands the need for the plural in conjunction with the hasMany() method.

And don't worry about those pesky words in the English language that aren't pluralized by just adding an "s" to the end. Wheels is smart enough to know that words like "deer" and "children" are the plurals of "deer" and "child," respectively.

An Example of hasOne

Let's consider an association between user and profile. A lot of websites allow you to enter required info such as name and email but also allow you to add optional information such as age, salary, and so on. These can of course be stored in the same table. But given the fact that so much information is optional, it would make sense to have the required info in a users table and the optional info in a profiles table. This gives us a hasOne() relationship between these two models: "A user has one profile."

In this case, our profile model would look like this:

app/models/profile.cfc
component extends="Model" {

    function config() {
        belongsTo("user");
    }

}

And our user model would look like this:

app/models/user.cfc
component extends="Model" {

    function config() {
        hasOne("profile");
    }

}

As you can see, you do not pluralize "profile" in this case because there is only one profile.

By the way, as you can see above, the association goes both ways, i.e. a user hasOne() profile, and a profile belongsTo() a user. Generally speaking, all associations should be set up this way. This will give you the fullest API to work with in terms of the methods and arguments that Wheels makes available for you.

Dependencies

A dependency is when an associated model relies on the existence of its parent. In the example above, a profile is dependent on a user. When you delete the user, you would usually want to delete the profile as well.

Wheels makes this easy for you. When setting up your association, simply add the argument dependent with one of the following values, and Wheels will automatically deal with the dependency.

In your model.cfc file's config() function::

// Instantiates the `profile` model and calls its `delete()` method.
hasOne(name="profile", dependent="delete");

// Quickly deletes the `profile` without instantiating it.
hasOne(name="profile", dependent="deleteAll");

// Sets the `userId` of the profile to `NULL`.
hasOne(name="profile", dependent="remove");

// Sets the `userId` of the profile to `NULL` (without instantiation).
hasOne(name="profile", dependent="removeAll");

You can create dependencies on hasOne() and hasMany() associations, but not belongsTo().

Self-Joins

It's possible for a model to be associated to itself. Take a look at the below setup where an employee belongs to a manager for example:

app/models/employee.cfc
component extends="Model" {

    function config() {
        belongsTo(name="manager", modelName="employee", foreignKey="managerId");
    }

}

Both the manager and employee are stored in the same employees table and share the same Employee model.

When you use this association in your code, the employees table will be joined to itself using the managerid column. Wheels will handle the aliasing of the (otherwise duplicated) table names. It does this by using the pluralized version of the name you gave the association (in other words "managers" in this case).

This is important to remember because if you, for example, want to select the manager's name, you will have to do so manually (Wheels won't do this for you, like it does with normal associations) using the select argument.

Here's an example of how to select both the name of the employee and their manager:

app/controllers/employees.cfc
component extends="Controller" {
 function index() {

   employees= model("employee").findAll(include="manager", select="employees.name, managers.name AS managerName");

 }
}

Know Your Joins

Because the default joinType for belongsTo() is inner, employees without a manager assigned to them will not be returned in the findAll() call above. To return all rows you can set jointype to outer instead.

Database Table Setup

Like everything else in Wheels, we strongly recommend a default naming convention for foreign key columns in your database tables.

In this case, the convention is to use the singular name of the related table with id appended to the end. So to link up our table to the employees table, the foreign key column should be named employeeid.

Breaking the Convention

Wheels offers a way to configure your models to break this naming convention, however. This is done by using the foreignKey argument in your models' belongsTo() calls.

Let's pretend that you have a relationship between author and post, but you didn't use the naming convention and instead called the column author_id. (You just can't seem to let go of the underscores, can you?)

Your post's config() method would then need to look like this:

app/models/post.cfc
component extends="Model" {

    function config() {
        belongsTo(name="author", foreignKey="author_id");
    }

}

You can keep your underscores if it's your preference or if it's required of your application.

Leveraging Model Associations in Your Application

Now that we have our associations set up, let's use them to get some data into our applications.

There are a couple ways to join data via associations, which we'll go over now.

Using the include Argument in findAll()

Here's what that call would look like:

app/controllers/posts.cfc
component extends="Controller" {
 function index() {

   posts = model("post").findAll(include="author");

 }
}

It's that simple. Wheels will then join the authors table automatically so that you can use that data along with the data from posts.

Note that if you switch the above statement around like this:

app/controllers/authors.cfc
component extends="Controller" {
 function index() {

  authors = model("author").findAll(include="posts");

 }
}

Then you would need to specify "post" in its plural form, "posts." If you're thinking about when to use the singular form and when to use the plural form, just use the one that seems most natural.

If you look at the two examples above, you'll see that in example #1, you're asking for all posts including each post's author (hence the singular "author"). In example #2, you're asking for all authors and all of the posts written by each author (hence the plural "posts").

You're not limited to specifying just one association in the include argument. You can for example return data for authors, posts, and bios in one call like this:

app/controllers/authors.cfc
component extends="Controller" {
 function index() {

   authorsPostsAndComments =    model("author").findAll(include="posts,bio");

 }
}

To include several tables, simply delimit the names of the models with a comma. All models should contain related associations, or else you'll get a mountain of repeated data back.

Joining Tables Through a Chain of Associations

When you need to include tables more than one step away in a chain of joins, you will need to start using parenthesis. Look at the following example:

app/controllers/comments.cfc
component extends="Controller" {
 function index() {

   commentsPostsAndAuthors = model("comment").findAll(include="post(author)");

 }
}

The use of parentheses above tells Wheels to look for an association named author on the post model instead of on the comment model. (Looking at the comment model is the default behavior when not using parenthesis.)

Handling Column Naming Collisions

There is a minor caveat to this approach. If you have a column in both associated tables with the same name, Wheels will pick just one to represent that column.

In order to include both columns, you can override this behavior with the select argument in the finder functions.

For example, if we had a column named name in both your posts and authors tables, then you could use the select argument like so:

app/controllers/posts.cfc
component extends="Controller" {
 function index() {

   posts = model("post").findAll(
    select="posts.name, authors.id, authors.post_id, authors.name AS authorname",
    include="author"
);

 }
}

You would need to hard-code all column names that you need in that case, which does remove some of the simplicity. There are always trade-offs!

Using Dynamic Shortcut Methods

A cool feature of Wheels is the ability to use dynamic shortcut methods to work with the models you have set up associations for. By dynamic, we mean that the name of the method depends on what name you have given the association when you set it up. By shortcut, we mean that the method usually delegates the actual processing to another Wheels method but gives you, the developer, an easier way to achieve the task (and makes your code more readable in the process).

As usual, this will make more sense when put into the context of an example. So let's do that right now.

Example: Dynamic Shortcut Methods for Posts and Comments

Listing of Dynamic Shortcut Methods

Here are all the methods that are added for the three possible association types.

Methods Added by hasMany

Replace XXX below with the name of the associated model (i.e. comments in the case of the example that we're using here).

Method
Example
Description

XXX()

post.comments()

Returns all comments where the foreign key matches the post's primary key value. Similar to calling model("comment").findAll(where="postid=#post.id#").

addXXX()

post.addComment(comment)

Adds a comment to the post association by setting its foreign key to the post's primary key value. Similar to calling model("comment").updateByKey(key=comment.id, postid=post.id).

removeXXX()

post.removeComment(comment)

Removes a comment from the post association by setting its foreign key value to NULL. Similar to calling model("comment").updateByKey(key=comment.id, postid="").

deleteXXX()

post.deleteComment(comment)

Deletes the associated comment from the database table. Similar to calling model("comment").deleteByKey(key=comment.id).

removeAllXXX()

post.removeAllComments()

Removes all comments from the post association by setting their foreign key values to NULL. Similar to calling model("comment").updateAll(postid="", where="postid=#post.id#").

deleteAllXXX()

post.deleteAllComments()

Deletes the associated comments from the database table. Similar to calling model("comment").deleteAll(where="postid=#post.id#").

XXXCount()

post.commentCount()

Returns the number of associated comments. Similar to calling model("comment").count(where="postid=#post.id#").

newXXX()

post.newComment()

Creates a new comment object. Similar to calling model("comment").new(postid=post.id).

createXXX()

post.createComment()

Creates a new comment object and saves it to the database. Similar to calling model("comment").create(postid=post.id).

hasXXX()

post.hasComments()

Returns true if the post has any comments associated with it. Similar to calling model("comment").exists(where="postid=#post.id#").

findOneXXX()

post.findOneComment()

Returns one of the associated comments. Similar to calling model("comment").findOne(where="postid=#post.id#").

Methods Added by hasOne

Method
Example
Description

XXX()

author.profile()

Returns the profile where the foreign key matches the author's primary key value. Similar to calling model("profile").findOne(where="authorid=#author.id#").

setXXX()

author.setProfile(profile)

Sets the profile to be associated with the author by setting its foreign key to the author's primary key value. You can pass in either a profile object or the primary key value of a profile object to this method. Similar to calling model("profile").updateByKey(key=profile.id, authorid=author.id).

removeXXX()

author.removeProfile()

Removes the profile from the author association by setting its foreign key to NULL. Similar to calling model("profile").updateOne(where="authorid=#author.id#", authorid="").

deleteXXX()

author.deleteProfile()

Deletes the associated profile from the database table. Similar to calling model("profile").deleteOne(where="authorid=#author.id#")

newXXX()

author.newProfile()

Creates a new profile object. Similar to calling model("profile").new(authorid=author.id).

createXXX()

author.createProfile()

Creates a new profile object and saves it to the database. Similar to calling model("profile").create(authorid=author.id).

hasXXX()

author.hasProfile()

Returns true if the author has an associated profile. Similar to calling model("profile").exists(where="authorid=#author.id#").

Methods Added by belongsTo

Method
Example
Description

XXX()

comment.post()

Returns the post where the primary key matches the comment's foreign key value. Similar to calling model("post").findByKey(comment.postid).

hasXXX()

comment.hasPost()

Returns true if the comment has a post associated with it. Similar to calling model("post").exists(comment.postid).

One general rule for all of the methods above is that you can always supply any argument that is accepted by the method that the processing is delegated to. This means that you can, for example, call post.comments(order="createdAt DESC"), and the order argument will be passed along to findAll().

Another rule is that whenever a method accepts an object as its first argument, you also have the option of supplying the primary key value instead. This means that author.setProfile(profile) will perform the same task as author.setProfile(1). (Of course, we're assuming that the profile object in this example has a primary key value of 1.)

Performance of Dynamic Association Finders

You may be concerned that using a dynamic finder adds yet another database call to your application.

If it makes you feel any better, all calls in your Wheels request that generate the same SQL queries will be cached for that request. No need to worry about the performance implications of making multiple calls to the same author.posts() call in the scenario above, for example.

Passing Arguments to Dynamic Shortcut Methods

You can also pass arguments to dynamic shortcut methods where applicable. For example, with the XXX() method, perhaps we'd want to limit a post's comment listing to just ones created today. We can pass a where argument similar to what is passed to the findAll() function that powers XXX() behind the scenes.

today = DateFormat(Now(), "yyyy-mm-dd");
comments = post.comments(where="createdAt >= '#today# 00:00:00'");

Many-to-Many Relationships

We can use the same 3 association functions to set up many-to-many table relationships in our models. It follows the same logic as the descriptions mentioned earlier in this chapter, so let's jump right into an example.

Let's say that we wanted to set up a relationship between customers and publications. A customer can be subscribed to many publications, and publications can be subscribed to by many customers. In our database, this relationship is linked together by a third table called subscriptions (sometimes called a bridge entity by ERD snobs).

Setting up the Models

Here are the representative models:

app/models/Customer.cfc
component extends="Model" {

    function config() {
        hasMany("subscriptions");
    }

}
app/models/Publication.cfc
component extends="Model" {

    function config() {
        hasMany("subscriptions");
    }

}
app/models/Subscription.cfc
component extends="Model" {

    function config() {
      belongsTo("customer");
      belongsTo("publication");
    }

}

This assumes that there are foreign key columns in subscriptions called customerid and publicationid.

Using Finders with a Many-to-Many Relationship

At this point, it's still fairly easy to get data from the many-to-many association that we have set up above.

We can include the related tables from the subscription bridge entity to get the same effect:

app/controllers/subscriptions.cfc
component extends="Controller" {
 function index() {

   subscriptions= model("subscription").findAll(include="customer,publication");

 }
}

Creating a Shortcut for a Many-to-Many Relationship

app/models/customer.cfc
component extends="Model" {

 function config() {
    hasMany(name="subscriptions", shortcut="publications");
 }
}

Now you can get a customer's publications directly by using code like this:

app/controllers/customers.cfc
component extends="Controller" {
 function edit() {

   customer= model("customer").findByKey(params.key);
   publications= customer.publications();

 }
}

It also relies on the association names being consistent, but if you have customized your association names, you can specify exactly which associations the shortcut method should use with the through argument.

Sound complicated? That's another reason to stick to the conventions whenever possible: it keeps things simple.

Are You Still with Us?

As you just read, Wheels offers a ton of functionality to make your life easier in working with relational databases. Be sure to give some of these techniques a try in your next Wheels application, and you'll be amazed at how little code that you'll need to write to interact with your database.

Nested Properties

Save data in associated model objects through the parent.

When you're starting out as a Wheels developer, you are probably amazed at the simplicity of a model's CRUD methods. But then it all gets quite a bit more complex when you need to update records in multiple database tables in a single transaction.

One-to-One Relationships with Nested Properties

Consider a user model that has one profile:

app/models/User.cfc
component extends="Model" {

  function config() {
        hasOne("profile");
        nestedProperties(associations="profile");
    }

}

Setting up Data for the user Form in the Controller

First, in our controller, let's set the data needed for our form:

app/controllers/User.cfc
// In app/controllers/User.cfc
function new() {
    var newProfile = model("profile").new();
    user = model("user").new(profile=newProfile);
}

Because our form will also expect an object called profile nested within the user object, we must create a new instance of it and set it as a property in the call to user.new().

Also, because we don't intend on using the particular newProfile object set in the first line of the action, we can var scope it to clearly mark our intentions for its use.

If this were an edit action calling an existing object, our call would need to look similar to this:

app/controllers/User.cfc
function edit() {
    user = model("user").findByKey(key=params.key, include="profile");
}

Because the form will also expect data set in the profile property, you must include that association in the finder call with the include argument.

Building a Form for Posting Nested Properties

For this example, our form at app/views/users/new.cfm will end up looking like this:

app/views/users/new.cfm
#startFormTag(action="create")#

    <!--- Data for user model --->
    #textField(label="First Name", objectName="user", property="firstName")#
    #textField(label="Last Name", objectName="user", property="lastName")#

    <!--- Data for associated profile model --->
    #textField(
        label="Twitter Handle",
        objectName="user",
        association="profile",
        property="twitterHandle"
    )#
    #textArea(
        label="Biography",
        objectName="user",
        association="profile",
        property="bio"
    )#

    <div>#submitTag(value="Create")#</div>

#endFormTag()#

Of note are the calls to form helpers for the profile model, which contain an extra argument for association. This argument is available for all object-based form helpers. By using the association argument, Wheels will name the form field in such a way that the properties for the profile will be nested within an object in the user model.

Take a minute to read that last statement again. OK, let's move on to the action that handles the form submission.

Saving the Object and Its Nested Properties

You may be surprised to find out that our standard create action does not change at all from what you're used to.

app/controllers/Users.cfc
function create() {
    user = model("user").new(params.user);
    if ( user.save() ) {
        flashInsert(success="The user was created successfully.");
        redirectTo(controller=params.controller);
    } else {
        renderView(action="new");
    }
}

When calling user.save() in the example above, Wheels takes care of the following:

  • Saves the data passed into the user model.

  • Sets a property on user called profile with the profile data stored in an object.

  • Saves the data passed into that profile model.

  • Wraps all calls in a transaction in case validations on any of the objects fail or something wrong happens with the database.

For the edit scenario, this is what our update action would look like (which is very similar to create):

app/controllers/Users.cfc
function update() {
    user = model("user").findByKey(params.user.id);
    if ( user.update(params.user) ) {
        flashInsert(success="The user was updated successfully.");
        redirectTo(action="edit");
    } else {
        renderView(action="edit");
    }
}

One-to-Many Relationships with Nested Properties

Nested properties work with one-to-many associations as well, except now the nested properties will contain an array of objects instead of a single one. We know that one user can have many addresses. Furthermore, we know that each user has only one profile.

In the user model, let's add an association called addresses and also enable it as nested properties.

app/models/User.cfc
component extends="Model" {

 function config() {
        hasOne("profile");
        hasMany("addresses");
        nestedProperties(
            associations="profile,addresses",
            allowDelete=true
        );
    }

}

The addresses table contains a foreign key to the Users table called userid, Now in the addresses model, let's associate it with its parent User and also enable it as nested properties.

app/models/Address.cfc
component extends="Model" {

 function config() {
        belongsTo("User");
        nestedProperties(
            associations="User",
            allowDelete=true
        );
    }

}

Setting up Data for the user Form in the Controller

Setting up data for the form is similar to the one-to-one scenario, but this time the form will expect an array of objects for the nested properties instead of a single object.

In this example, we'll just put one new address in the array.

app/controllers/Users.cfc
function new() {
    var newAddresses = [ model("address").new() ];
    user = model("user").new(addresses=newAddresses);
}

In the edit scenario, we just need to remember to call the include argument to include the array of addresses saved for the particular user:

app/controllers/Users.cfc
function edit() {
    user = model("user").findByKey(key=params.key, include="addresses");
}

Building the Form for the One-to-Many Association

This time, we'll add a section for addresses on our form:

app/views/users/_form.cfm
#startFormTag(action="create")#

    <!--- Data for `user` model --->
    <fieldset>
        <legend>User</legend>

        #textField(label="First Name", objectName="user", property="firstName")#
        #textField(label="Last Name", objectName="user", property="lastName")#
    </fieldset>

    <!--- Data for `address` models --->
    <fieldset>
        <legend>Addresses</legend>

        <div id="addresses">
            #includePartial(user.addresses)#
        </div>
    </fieldset>

    <div>#submitTag(value="Create")#</div>

#endFormTag()#

In this case, you'll see that the form for addresses is broken into a partial. (See the chapter on Partials for more details.) Let's take a look at that partial.

The _address Partial

Here is the code for the partial at app/views/users/_address.cfm. Wheels will loop through each address in your nested properties and display this piece of code for each one.

app/views/users/_address.cfm
<div class="address">
    #textField(
        label="Street",
        objectName="user",
        association="addresses",
        position=arguments.current,
        property="address1"
    )#
    #textField(
        label="City",
        objectName="user",
        association="addresses",
        position=arguments.current,
        property="city"
    )#
    #textField(
        label="State",
        objectName="user",
        association="addresses",
        position=arguments.current,
        property="state"
    )#
    #textField(
        label="Zip",
        objectName="user",
        association="addresses",
        position=arguments.current,
        property="zip"
    )#
</div>

Because there can be multiple addresses on the form, the form helpers require an additional argument for position. Without having a unique position identifier for each address, Wheels would have no way of understanding which state field matches with which particular address, for example.

Here, we're passing a value of arguments.current for position. This value is set automatically by Wheels for each iteration through the loop of addresses.

Auto-saving a Collection of Child Objects

Basically, your typical code to save the user and its addresses is exactly the same as the code demonstrated in the Saving the Object and Its Nested Properties section earlier in this chapter.

Writing the action to save the data is clearly the easiest part of this process!

Transactions are Included by Default

As mentioned earlier, Wheels will automatically wrap your database operations for nested properties in a transaction. That way if something goes wrong with any of the operations, the transaction will rollback, and you won't end up with incomplete saves.

Many-to-Many Relationships with Nested Properties

We all enter the scenario where we have "join tables" where we need to associate models in a many-to-many fashion. Wheels makes this pairing of entities simple with some form helpers.

//  app/models/Customer.cfc
component extends="Model" {

    public function config() {
        hasMany(name="subscriptions", shortcut="publications");
    }

}
//  app/models/Publication.cfc
component extends="Model" {

    public function config() {
        hasMany("subscriptions");
    }

}
//  app/models/Subscription.cfc
component extends="Model" {

    public function config() {
        belongsTo("customer");
        belongsTo("publication");
    }

}

Setting up the Nested Properties in the Model

Here is how we would set up the nested properties in the customer model for this example:

app/models/Customer.cfc
component extends="Model" {

    public function config() {
        //  Associations
        hasMany(name="subscriptions", shortcut="publications");
        //  Nested properties
        nestedProperties(
            associations="subscriptions",
            allowDelete=true
        );
    }

}

Setting up Data for the customer Form in the Controller

Let's define the data needed in an edit action in the controller at app/controllers/Customers.cfc.

app/controllers/Customers.cfc
function edit() {
    customer = model("customer").findByKey(
        key=params.key,
        include="subscriptions"
    );
    publications = model("publication").findAll(order="title");
}

For the view, we need to pull the customer with its associated subscriptions included with the include argument. We also need all of the publications in the system for the user to choose from.

Building the Many-to-Many Form

We can now build a series of check boxes that will allow the end user choose which publications to assign to the customer.

The view template at app/views/customers/edit.cfm is where the magic happens. In this view, we will have a form for editing the customer and check boxes for selecting the customer's subscriptions.

app/views/customers/edit.cfm
<cfparam name="customer">
<cfparam name="publications" type="query">

<cfoutput>

#startFormTag(action="update")#

<fieldset>
    <legend>Customer</legend>

    #textField(
        label="First Name",
        objectName="customer",
        property="firstName"
    )#
    #textField(
        label="Last Name",
        objectName="customer",
        property="lastName"
    )#
</fieldset>

<fieldset>
    <legend>Subscriptions</legend>

    <cfloop query="publications">
        #hasManyCheckBox(
            label=publications.title,
            objectName="customer",
            association="subscriptions",
            keys="#customer.key()#,#publications.id#"
        )#
    </cfloop>
</fieldset>

<div>
    #hiddenField(objectName="customer", value="id")#
    #submitTag()#
</div>

#endFormTag()#

</cfoutput>

The keys argument accepts the foreign keys that should be associated together in the subscriptions join table. Note that these keys should be listed in the order that they appear in the database table. In this example, the subscriptions table in the database contains a composite primary key with columns called customerid and publicationid, in that order.

How the Form Submission Works

Handling the form submission is the most powerful part of the process, but like all other nested properties scenarios, it involves no extra effort on your part.

You'll notice that this example update action is fairly standard for a Wheels application:

app/controllers/Customers.cfc
function update() {
    //  Load customer object
    customer = model("customer").findByKey(params.customer.id);
    /*  If update is successful, generate success message
        and redirect back to edit screen */
    if ( customer.update(params.customer) ) {
        redirectTo(
            action="edit",
            key=customer.id,
            success="#customer.firstName# #customer.lastName# record updated successfully."
        );
        //  If update fails, show form with errors 
    } else {
        renderView(action="edit");
    }
}

In fact, there is nothing special about this. But with the nested properties defined in the model, Wheels handles quite a bit when you save the parent customer object:

  • Wheels will update the customers table with any changes submitted in the Customers <fieldset>.

  • Wheels will add and remove records in the subscriptions table depending on which check boxes are selected by the user in the Subscriptions <fieldset>.

Transactions

Wheels automatically wraps your database calls in transactions to assist your application in maintaining data integrity. Learn how to control this functionality.

Database transactions are a way of grouping multiple queries together. They are useful in case the outcome of one query depends on the completion of another. For example, if you want to take money from one person's bank account, and transfer it into someone else's, you probably want to make sure the debit completes before running the credit.

You'll be pleased to know that Wheels makes using database transactions easy. In fact, the vast majority of the time, you won't need to think about using them at all because Wheels automatically runs all queries within the callback chain as a single transaction for creates, updates, and deletes.

If any of the callbacks within the chain return false, none of the queries will commit.

For example, say you want to automatically create the first post when a new author subscribes to a blog.

In your Author model, you would add the following code:

Author.cfc
component extends="Model" {
  function config(){
    afterCreate("createFirstPost");
  }

  function createFirstPost(){
    var post = model("post").new(
        authorId=this.id,
        text="This is my first post!";
        post.save();
  }
}

In this example, if the post doesn't save (perhaps due to a validation problem), the author doesn't get created either. This helps to maintain database integrity.

Disabling Transactions

If you want to manage transactions yourself using the <cftransaction> tag, you can simply add transaction=falseto any CRUD method.

model("author").create(name="John", transaction=false);

Another option is to disable transactions across your entire application using the transactionMode configuration:

app/config/settings.cfm
set(transactionMode=false);

Using Rollbacks

Sometimes it's useful to use a rollback to test a process without making any permanent changes to the database. To do this, add transaction="rollback" to any CRUD method.

model("author").create(name="John", transaction="rollback");

Again, to configure your entire application to rollback all transactions, you can set the transactionMode configuration to rollback.

app/config/settings.cfm
set(transactionMode="rollback");

Nesting Transactions with invokeWithTransaction

invokeWithTransaction(
    method="transferFunds",
    personFrom=david,
    personTo=mary,
    amount=100
);

Calculated Properties

Generate extra properties in your models on the fly without needing to store redundant data in your database.

Working within CFML's Constraints to Deliver OOP-like Functionality

Wheels makes up for the slowness of arrays of objects in CFML by providing calculated properties. With calculated properties, you can generate additional properties on the fly based on logic and data within your database.

Example #1: Full Name

Consider the example of fullName. If your database table has fields for firstName and lastName, it wouldn't make sense to store a third column called fullName. This would require more storage for redundant data, and it would add extra complexity that could lead to bugs and maintenance problems in the future.

Traditional Object-Oriented Calculations

In most object-oriented languages, you would add a method to your class called getFullName(), which would return the concatenation of this.firstName & " " & this.lastName. The getFullName() method could potentially provide arguments to list the last name first and other types of calculations or transformations as well.

Using Calculated Properties to Generate fullName in the Database at Runtime

As an alternative, you can set up a calculated property that dynamically performs the concatenation at the database level. In our example, we would write a line similar to this in our model's config() method:

property(
        name="fullName",
        sql="RTRIM(LTRIM(ISNULL(users.firstname, '') + ' '
            + ISNULL(users.lastname, '')))"
    );

As you can probably deduce, we're creating a SQL statement that will be run in the SELECT clause to generate the `fullName.

Example #2: Age

Naturally, if you store the user's birth date in the database, your application can use that data to dynamically calculate the user's age. Your app always knows how many years old the user is without needing to explicitly store his or her age.

Creating the Calculated Property for Age

In order to calculate an extra property called age based on the birthDate column, our calculated property in config() may look something like this:

Example Code

property(
        name="age",
        sql="(CAST(CONVERT(CHAR(8), GETDATE(), 112) AS INT)
            - CAST(CONVERT(CHAR(8), users.date_of_birth, 112) AS INT))
            / 10000"
);

Much like the fullName example above, this will cause the database to add a property called age storing the user's age as an integer.

Note that the cost to this approach is that you may need to introduce DBMS-specific code into your models. This may cause problems when you need to switch DBMS platforms, but at least all of this logic is isolated into your model CFCs.

Using the New age Property for Other Database Calculations

Calculated properties don't end at just generating extra properties. You can now also use the new property for additional calculations:

  • Creating additional properties with the select argument

  • Additional where clause calculations

  • Record sorting with order

  • Pagination

  • And so on…

For example, let's say that we only want to use age to return users who are in their 20s. We can use the new ageproperty as if it existed in the database table. For extra measure, let's also sort the results from oldest to youngest.

Example Code

users = model("user").findAll(
        where="age >= 20 AND age < 30", order="age DESC"
);

Specifying a Data Type

By default, calculated properties will return char as the column data type. Whilst this covers most scenarios, if you want to return something like a date, it can be problematic. Thankfully we can just specify a dataType argument to return the appropriate data type.

property(
  name="createdAtAlias",
  sql="posts.createdat", 
  dataType="datetime"
);

Object Validation

Wheels utilizes validation setup within the model to enforce appropriate data constraints and persistence. Validation may be performed for saves, creates, and updates.

Basic Setup

In order to establish the full cycle of validation, 3 elements need to be in place:

  • Model file containing business logic for the database table. Example: app/models/User.cfc

  • Controller file for creating, saving or updating a model instance. Example: app/controllers/Users.cfc

  • View file for displaying the original data inputs and an error list. Example: app/views/users/index.cfm

Note: Saving, creating, and updating model objects can also be done from the model file itself (or even in the view file if you want to veer completely off into the wild). But to keep things simple, all examples in this chapter will revolve around code in the controller files.

The Model

Validations are always defined in the config() method of your model. This keeps everything nice and tidy because another developer can check config() to get a quick idea on how your model behaves.

Let's dive right into a somewhat comprehensive example:

component extends="Model" output="false" {

 function config() {
        validatesPresenceOf(
            properties="firstName,lastName,email,age,password"
        );
        validatesLengthOf(properties="firstName,lastName", maximum=50);
        validatesUniquenessOf(property="email");
        validatesNumericalityOf(property="age", onlyInteger=true);
        validatesConfirmationOf(property="password");
    }

}

This is fairly readable on its own, but this example defines the following rules that will be run before a create, update, or save is called:

  • The firstName, lastName, email, age, and password fields must be provided, and they can't be blank.

  • At maximum, firstName and lastName can each be up to 50 characters long.

  • The value provided for email cannot already be used in the database.

  • The value for age can only be an integer.

  • password must be provided twice, the second time via a field called passwordConfirmation.

If any of these validations fail, Wheels will not commit the create or update to the database. As you'll see later in this chapter, the controller should check for this and react accordingly by showing error messages generated by the model.

Listing of Validation Functions

Automatic Validations

Now that you have a good understanding of how validations work in the model, here is a piece of good news. By default, Wheels will perform many of these validations for you based on how you have your fields set up in the database.

By default, these validations will run without your needing to set up anything in the model:

  • Date or time fields will be checked for the appropriate format.

Note these extra behaviors as well:

  • If you've already set a validation on a particular property in your model, the automatic validations will be overridden by your settings.

To disable automatic validations in your Wheels application, change this setting in app/config/settings.cfm:

set(automaticValidations=false);

Use when, condition, or unless to Limit the Scope of Validation

If you want to limit the scope of the validation, you have 3 arguments at your disposal: when, condition, and unless.

when Argument

The when argument accepts 3 possible values.

  • onSave (the default)

  • onCreate

  • onUpdate

To limit our email validation to run only on create, we would change that line to this:

validatesUniquenessOf(property="email", when="onCreate");

condition and unless Arguments

condition and unless provide even more flexibility when the when argument isn't specific enough for your validation's needs.

Each argument accepts a string containing an expression to evaluate. condition specifies when the validation should be run. unless specifies when the validation should not be run.

As an example, let's say that the model should only verify a CAPTCHA if the user is logged out, but not when they enter their name as "Ben Forta":

validate(
    method="validateCaptcha",
    condition="not isLoggedIn()",
    unless="this.name is 'Ben Forta'"
);

Custom Validations

There is only one difference between how the different functions work:

To use a custom validation, we pass one of these functions a method or set of methods to run:

validate(method="validateEmailFormat");
private function validateEmailFormat() {
    if ( !IsValid("email", this.email) ) {
        addError(property="email", message="Email address != in a valid format.");
    }
}

Note that IsValid() is a function build into your CFML engine.

This is a simple rule, but you can surmise that this functionality can be used to do more complex validations as well. It's a great way to isolate complex validation rules into separate methods of your model.

Adding Errors to the Model Object as a Whole

As an example, here's a custom validation method that doesn't allow the user to sign up for an account between the hours of 3:00 and 4:00 am in the server's time zone:

private function disallowMaintenanceWindowRegistrations() {
    local.hourNow = DatePart("h", Now());
    if ( local.hourNow >= 3 && local.hourNow < 4 ) {
        local.timeZone = CreateObject("java", "java.util.TimeZone").getDefault();
        addErrorToBase(
                message="We're sorry, but we don't allow new registrations between
                the hours of 3:00 && 4:00 am #local.timeZone#."
            );
    }
}

Sure, we could add logic to the view to also not show the registration form, but this validation in the model would make sure that data couldn't be posted via a script between those hours as well. Better safe than sorry if you're running a public-facing application!

The Controller

The controller continues with the simplicity of validation setup, and at the most basic level requires only 5 lines of code to persist the form data or return to the original form page to display the list of errors.

component extends="Controller" {

    public function save() {
        //  User model from form fields via params
        newUser = model("user").new(params.newUser);
        //  Persist new user
        if ( newUser.save() ) {
            redirectTo(action="success");
            //  Handle errors
        } else {
            renderView(action="index");
        }
    }

}

The first line of the action creates a newUser based on the user model and the form inputs (via the params struct).

The View

Wheels factors out much of the error display code that you'll ever need. As you can see by this quick example, it appears to mainly be a normal form. But when there are errors in the provided model, Wheels will apply styles to the erroneous fields.

<cfoutput>

#errorMessagesFor("newUser")#

#startFormTag(action="save")#
    #textField(label="First Name", objectName="newUser", property="nameFirst")#
    #textField(label="Last Name", objectName="newUser", property="nameLast")#
    #textField(label="Email", objectName="newUser", property="email")#
    #textField(label="Age", objectName="newUser", property="age")#
    #passwordField(label="Password", objectName="newUser", property="password")#
    #passwordField(
        label="Re-type Password to Confirm", objectName="newUser",
        property="passwordConfirmation"
    )#
    #submitTag()#
#endFormTag()#

</cfoutput>

Error Messages

For your reference, here are the default error message formats for the different validation functions:

Function
Format

validatesConfirmationOf()

[property] should match confirmation

validatesExclusionOf()

[property] is reserved

validatesFormatOf()

[property] is invalid

validatesInclusionOf()

[property] is not included in the list

validatesLengthOf()

[property] is the wrong length

validatesNumericalityOf()

[property] is not a number

validatesPresenceOf()

[property] can't be empty

validatesUniquenessOf()

[property] has already been taken

Custom Error Messages

Wheels models provide a set of sensible defaults for validation errors. But sometimes you may want to show something different than the default.

There are 2 ways to accomplish this: through global defaults in your config files or on a per-property basis.

Setting Global Defaults for Error Messages

Using basic global defaults for the validation functions, you can set error messages in your config file at app/config/settings.cfm.

set(functionName="validatesPresenceOf", message="Please provide a value for [property]");

As you can see, you can inject the property's name by adding [property] to the message string. Wheels will automatically separate words based on your camelCasing of the variable names.

Setting an Error Message for a Specific Model Property

Another way of adding a custom error message is by going into an individual property in the model and adding an argument named message.

Here's a change that we may apply in the config() method of our model:

validatesNumericalityOf(
    property="email",
    message="Email address is already in use in another account"
);

Dirty Records

How to track changes to objects in your application.

Wheels provides some very useful methods for tracking changes to objects. You might think, Why do I need that? Won't I just know that I changed the object myself?

Well, that depends on the structure of your code.

As you work with Wheels and move away from that procedural spaghetti mess you used to call code to a better, cleaner object-oriented approach, you may get a sense that you have lost control of what your code is doing. Your new code is creating objects, they in turn call methods on other objects automatically, methods are being called from multiple places, and so on. Don't worry though, this is a good thing. It just takes a while to get used to, and with the help of some Wheels functionality, it won't take you that long to get used to it either.

An Example with Callbacks

Let's say you have used a callback to specify that a method should be called whenever a user object is saved to the database. You won't know exactly where this method was called from. It could have been the user doing it themselves on the website, or it could have been done from your internal administration area. Generally speaking, you don't need to know this either.

One thing your business logic might need to know though is a way to tell exactly what was changed on the object. Maybe you want to handle things differently if the user's last name was changed than if the email address was changed, for example.

Let's look at the methods Wheels provide to make tracking these changes easier for you.

Methods for Tracking Changes

Let's get to coding…

post = model("post").findByKey(1);
result = post.hasChanged();

By the way, when we are talking about "change" in Wheels, we always mean whether or not an object's properties have changed compared to what is stored in the columns they map to in the database table.

In the case of the above example, the result variable will contain false because we just fetched the object from the database and did not make any changes to it at all.

Well, let's make a change then. If we didn't, this chapter wouldn't be all that interesting, would it?

post.title = "A New Post Title";
result = post.hasChanged();

Now result will be true because what is stored in post.title differs from what is stored in the titlecolumn for this record in the posts table (well, unless the title was "A New Post Title" even before the change, in which case the result would still be false).

OK, let's save the object to the database now and see how that affects things.

post.save();
result = post.hasChanged();

Now result will once again contain false. When you save a changed (a.k.a. "dirty") object, it clears out its changed state tracking and is considered unchanged again.

Don't Forget the Context

All of the examples in this chapter look a little ridiculous because it doesn't make much sense to check the status of an object when you changed it manually in your code. As we said in the beginning of the chapter, when put into context of callbacks, multiple methods, etc., it will become clear how useful these methods really are.

Internal Use of Change Tracking

It's worth noting here that Wheels makes good use of this change tracking internally as well. If you make changes to an object, Wheels is smart enough to only update the changed columns, leaving the rest alone. This is good for a number of reasons but perhaps most importantly for database performance. In high traffic web applications, the bottleneck is often the database, and anything that can be done to prevent unnecessary database access is a good thing.

One "Gotcha" About Tracking Changes

Object Callbacks

Write code that runs every time a given object is created, updated, or deleted.

Callbacks in Wheels allow you to have code executed before and/or after certain operations on an object. This requires some further explanation, so let's go straight to an example of a real-world application: the e-commerce checkout.

A Real-World Example of Using Callbacks

Let's look at a possible scenario for what happens when a visitor to your imaginary e-commerce website submits their credit card details to finalize an order:

Let's say you want to have the following things executed somewhere in the code:

  • Stripping out dashes from the credit card number to make it as easy as possible for the user to make a purchase.

  • Calculating shipping cost based on the country the package will be sent to.

It's tempting to put this code right in the controller, isn't it? But if you think ahead a little, you'll realize that you might build an administrative interface for orders and maybe an express checkout as well at some point in the future. You don't want to duplicate all your logic in all these places, do you?

Object callbacks to the rescue! By using object callbacks to implement this sort of logic in your model, you keep it out of your controllers and ensure your code stays DRY (Don't Repeat Yourself).

Part of the Order.cfc model file:

Order.cfc
component extends="Model" {

    public function config() {
        beforeValidationOnCreate("fixCreditCard");
        afterValidation("calculateShippingCost");
    }

    public function fixCreditCard() {

        writeOutput("Code for stripping out dashes in credit card numbers goes here...");
    }

    public function calculateShippingCost() {

        writeOutput("Code for calculating shipping cost goes here...");
    }

}

The above code registers 2 methods to be run at specific points in the life cycle of all objects in your application.

Use Proper Naming

When naming your callbacks you might be tempted to try and keep things (too) simple by doing something like afterValidation("afterValidation").

Do not do this.

If you do, Wheels will fail silently and you might be left wondering why nothing is happening. (What is happening is that you, if there is a corresponding method named afterValidation, unintentionally overrode an internal Wheels method.)

It's best to name the methods so they describe what task they actually perform, such as calculateShippingCost or fixCreditCard as shown in the example above.

Registering and Controlling Callbacks

The following 16 functions can be used to register callbacks.

Callback Life Cycle

As you can see above, there are a few places (5, to be exact) where one callback or the other will be executed, but not both.

The remaining callbacks get executed depending on whether or not we're running a "create," "update," or "delete" operation.

Breaking a Callback Chain

Order of Callbacks

Sometimes you need to run more than one method at a specific point in the object's life cycle. You can do this by passing in a list of method names like this:

beforeSave("checkSomething,checkSomethingElse");

When an object is saved in your application, these two callbacks will be executed in the order that you registered them. The checkSomething method will be executed first, and unless it returns false, the checkSomethingElse method will be executed directly afterward.

Special Case #1: findAll() and the afterFind() Callback

Column Types

We recommend that you respect the query column types. If you have a date / time value in the query, don't try to change it to a string for example. Some engines will allow it while others won't.

Does that sound complicated? This example should clear it up a little. Let's show some code to display how you can handle setting a fullName property on a hypothetical artist model.

component extends="Model" output="false" {

    public function config() {
        afterFind("setFullName");
    }

    public function setFullName() {
        arguments.fullName = "";
        if ( StructKeyExists(arguments, "firstName")
            && StructKeyExists(arguments, "lastName") ) {
            arguments.fullName = Trim(
                arguments.firstName & " " & arguments.lastName
            );
        }
        return arguments;
    }

}

In our example model, an artist's name can consist of both a first name and a last name ("John Mayer") or just the band / last name ("Abba."). The setFullName() method handles the logic to concatenate the names.

Always remember to return the arguments struct, otherwise Wheels won't be able to tell that you actually wanted to make any changes to the query.

Note that callbacks set on included models are not executed. Look at this example:

fooBars = model("foo").findAll(include="bars");

That will cause callback to be executed on the Foo model but not the Bar model.

Special Case # 2: Callbacks and the updateAll() and deleteAll() Methods

Installing and Using Plugins

Extend Wheels functionality by using plugins.

Wheels is a fairly lightweight framework, and we like to keep it that way. We won't be adding thousands of various features to Wheels just because a couple of developers find them "cool." ;)

Our intention is to only have functionality we consider "core" inside of Wheels itself and then encourage the use of plugins for everything else.

By using plugins created by the community or yourself, you're able to add brand new functionality to Wheels or completely change existing features. The possibilities are endless.

Manually Installing and Uninstalling Plugins

This couldn't be any simpler. To install a plugin, just download the plugin's zip file and drop it in the app/plugins folder.

If you want to remove it later simply delete the zip file. (Wheels will clean up any leftover folders and files.)

Reloading Wheels is required when installing/uninstalling. (Issue a reload=true request.)

Installing via Commandbox

With the Wheels CLI installed, you can just do:

This will present a list of available plugins. To install one, simply take note of the "Slug" and run with the install command.

When run in the root of a Wheels application, it should automatically add the plugin to /app/plugins and generate a .zip file with the corresponding name and version number.

File Permissions on plugins Folder

You may need to change access permissions on your application's app/plugins folder so that Wheels can write the subfolders and files that it needs to run. If you get an error when testing out a plugin, you may need to loosen up the permission level.

Plugin Naming, Versioning, and Dependencies

When you download plugins, you will see that they are named something like this: Scaffold-0.1.zip. In this case, 0.1 is the version number of the Scaffold plugin. If you drop both Scaffold-0.1.zip and Scaffold-0.2.zip in the plugins folder, Wheels will use the one with the highest version number and ignore any others.

If you try to install a plugin that is not compatible with your installed version of Wheels or not compatible with a previously installed plugin (i.e., they try to add/override the same functions), Wheels will throw an error on application start.

If you install a plugin that depends on another plugin, you will get a warning message displayed in the debug area. This message will name the plugin that you'll need to download and install to make the originally installed plugin work correctly.

The debug area will also show the version number of the plugin if the plugin Author has included a suitable box.json file.

Due Diligence

Plugins are very powerful, remember, they can completely override other functions, including Wheels core functions and functions of other installed plugins. For this reason we recommend that you take a look at the code itself for the plugins that you intend to use. This is especially important if you have multiple plugins that override the same function. In those cases you'll have to determine if the plugins play well with each other (which they typically do if they run their code and then defer back to the Wheels core function afterwards) or if they clash and cause problems (in which case you can perhaps contribute to the plugin repository in an effort to make the plugins behave better in situations like this).

Available Plugins

Automatic Time Stamps

Let Wheels handle time stamping of records.

When working with database tables, it is very common to have a column that holds the time that the record was added or last modified. If you have an e-commerce website with an orders table, you want to store the date and time the order was made; if you run a blog, you want to know when someone left a comment; and so on.

As with anything that is a common task performed by many developers, it makes a good candidate for abstracting to the framework level. So that's what we did.

Columns Used for Timestamps

If you have either of the following columns in your database table, Wheels will see them and treat them a little differently than others.

createdat

updatedat

Data Type of Columns

If you add any of these columns to your table, make sure they can accept date/time values (like datetime or timestamp, for example) and that they can be set to null.

Time Zones

Time stamping is done in UTC (Coordinated Universal Time) by default but if you want to use your local time instead all you have to do is change the global setting for it like this:

set(timeStampMode="local");

Using Multiple Data Sources

How to use more than one database in your Wheels application.

Sometimes you need to pull data from more than one database, whether it's by choice (for performance or security reasons, perhaps) or because that's the way your infrastructure is set up. It's something you have to find a way to deal with.

Using the dataSource() Function

Here's an example of a model file:

It's important to note that in order for Wheels to use the data source, it must first be configured in your respective CFML engine (i.e. in the Adobe ColdFusion, Lucee Admin etc).

Does Not Work with Associations

One thing to keep in mind when using multiple data sources with Wheels is that it doesn't work across associations. When including another model within a query, Wheels will use the calling model's data source for the context of the query.

Let's say you have the following models set up:

Developing Plugins

Extend Wheels functionality by creating a plugin.

Plugins are the recommended way to get new code accepted into Wheels. If you have written code that you think constitutes core functionality and should be added to Wheels, please create a plugin. After the community has used it for a while, it will be a simple task for us to integrate it into the Wheels core.

To create a plugin named MyPlugin, you will need to create a MyPlugin.cfc and an index.cfm file. Then zip these together as MyPlugin-x.x.zip, where x.x is the version number of your plugin.

The only other requirement to make a plugin work is that MyPlugin.cfc must contain a method named init. This method must set a variable called this.version, specifying the Wheels version the plugin is meant to be used on (or several Wheels version numbers in a list) and then return itself.

Here's an example:

Init() not config()

Note that plugins still use init() rather than config()

The index.cfm file is the user interface for your plugin and will be viewable when clicking through to it from the debug area of your application.

Using a Plugin to Add or Alter Capabilities

A plugin can add brand new functions to Wheels or override existing ones. A plugin can also have a simple one-page user interface so that the users of the plugin can provide input, display content, etc.

To add or override a function, you simply add the function to MyPlugin.cfc, and Wheels will inject it into Wheels on application start.

Please note that all functions in your plugin need to be public (access="public"). If you have functions that should only be called from the plugin itself, we recommend starting the function name with the $character (this is how many internal Wheels functions are named as well) to avoid any naming collisions.

It is also important to note that although you can overwrite functions, they are still available for you to leverage with the use of core.functionName().

Example: Overriding timeAgoInWords()

Plugin Attributes

There are 3 attributes you can set on your plugin to customize its behavior. The first and most important one is the mixin attribute.

By default, the functions in your plugins will be injected into all Wheels objects (controller, model, etc.). This is usually not necessary, and to avoid this overhead, you can use the mixin attribute to specify exactly where the functions should be injected. If you have a function that should be available in a controller (or view) this is how it could look:

The mixin attribute can be set either on the cfcomponent tag or on the individual cffunction tags.

The following values can be used for the mixin attribute: application, global, none, controller, model, dispatch, microsoftsqlserver, mysql, oracle, postgresql.

Another useful attribute is the environment attribute. Using this, you can tell Wheels that a plugin should only inject its functions in certain environment modes. Here's an example of that, taken from the Scaffold plugin, which doesn't need to inject any functions to Wheels at all when it's running in production mode.

Finally, there is a way to specify that your plugin needs another plugin installed in order to work. Here's an example of that:

Making Plugin Development More Convenient with Wheels Settings

When your Wheels application first initializes, it will unzip and cache the zip files in the app/plugins folder. Each plugin then has its own expanded subfolder. If a subfolder exists but has no corresponding zip file, Wheels will delete the folder and its contents.

This is convenient when you're deploying plugins but can be annoying when you're developing your own plugins. By default, every time you make a change to your plugin, you need to rezip your plugin files and reload the Wheels application by adding ?reload=true to the URL.

Disabling Plugin Overwriting

To force Wheels to skip the unzipping process, set the overwritePlugins setting to false in the development` environment.

With this setting, you'll be able to reload your application without worrying about your file being overwritten by the contents of the corresponding zip file.

Disabling Plugin Folder Deletion

To force Wheels to skip the folder deletion process, set the deletePluginDirectories setting to false for your development environment.

With this setting, you can now develop new plugins in your application without worrying about having a corresponding zip file in place.

Stand-Alone Plugins

If your plugin is completely stand-alone, you can call it from its view page using just the name of the plugin. This works because Wheels has created a pointer to the plugin object residing in the application scope. One example of a stand-alone plugin is the PluginManager. If you check out its view code, you will see that it calls itself like this:

Don't forget to comment!

With Wheels 2.x we can take advantage of the inbuilt documentation generator. Try and tag your public facing functions appropriately.

Here's an example from the wheels ical4J plugin:

The javaDoc style comments will automatically show this function under Plugins > Calendaring, rather than in the "Uncategorized" functions. The @parameter lines give a helpful hint to the user

Box.json

This saves you having to read in the box.json file, should you wish to use it for storing ancillary data and settings.

Automatic Java Lib Mappings

If you've ever wanted to do a quick wrapper for a java class/lib, this new feature in 2.x means you can add a .class or .jar file, and it will be automatically mapped into the this.javaSettings.loadpaths setting when the application starts.

This means you can distribute plugins with Java libs and they'll work properly without additional user intervention!

Enabling Travis CI Testing

One of the nicest things about 2.x is the tighter integration with command-line tools such as CommandBox. We can take advantage of the new testing suite JSON return type and the new Wheels CLI in CommandBox 2.x to easily build a Travis CI test. It's perhaps easiest to just show the .travis.yml file - this goes in the root of your gitHub plugin repository, and once you've turned on testing under Travis.org, will run your test suite on every commit.

In sum, this:

  • Installs CommandBox

  • Installs the Wheels CLI

  • Installs the master branch of the Wheels repository

  • Installs your plugin from your repository (rather than the forgebox version which will be the version behind)

  • Starts the local server

  • Runs the test suite, pointing only at your plugin's unit tests

Naturally, you could do more complex things with this, such as multiple CF Engines, but for a quick setup it's a good starting point!

Now Go Build Some Plugins!

Armed with this knowledge about plugins, you can now go and add that feature you've always wanted or change that behavior you've always hated. We've stripped you of any right to blame us for your discontents. :)

Publishing Plugins

How to publish your plugin to forgebox.io via CommandBox

As a plugin author, it's well worth spending a little time setting yourself up to work with forgebox with the minimum amount of effort. Once done, you'll be able to either publish directly from the commandline, or upload to forgebox manually.

This tutorial makes extensive use of CommandBox, GIT and the Wheels CLI.

Requirements

You'll also want the Wheels CLI. You can install that in CommandBox via install wheels-cli. This will also update it if you've got an older version installed.

Some scripted commands also require the git CLI, although these are technically optional.

Setup a forgebox user account

Once you've got your credentials, you should be good to go.

If you've already got an account, you need to login at least once, which will store an API token for future use:

Ensure you've got a box.json in your plugin root

Forgebox uses your local box.json - you'll need one! Critical package information like the name of your plugin and the location are stored here. You can create one manually, or you can run:

Ensure you've set some critical box.json attributes

In order for other Wheels users to quickly identify and install your plugin via the Wheels CLI, make sure you set the following box.json attributes - whilst a standard box.json might only have name, version,author, we need a little more information. Here's a template to get you started: (replace the values in CAPS)

Your completed box.json might look something like this:

Using the forgebox staging server (optional)

Remember this configuration will "stick", so make sure you change it back afterwards. (I find once changed, it might not kick in until you reload the CommandBox shell via r).

Publishing a plugin to forgebox

Both Wheels CLI and Forgebox are expecting a tagged release with the plugin contents (e.g. zip). So the best way to publish is to...

  1. Navigate into the plugin directory

  2. Ensure that directory is authorized to publish the repo (e.g. git remote -v should list your fetch/push endpoints)

Note: Git dislikes nested repos, so it's best to setup a test wheels site specifically for plugin development/deployment. Then git init within each plugin directory itself, but not at the root. (e.g. /app/plugins/PluginName/)

ForgeBox does not store your actual package files like npm, but points to your download location.

The following should happen (again, assuming you have git publish rights from that plugin directory)

  1. Auto increment your version number within box.json

  2. Push updated box.json to forgebox (with new version number + location)

  3. Create a git "Tagged Release" which is basically a zip containing the source files

Once you run this command, you can run forgebox show my-package to confirm it's there. If you change the slug, a new package will be created. If you change the version, a new version will be added to the existing package.

Adding a new version via publishing scripts

By adding the following block to our box.json, we can more easily deploy new versions with a single command:

Obviously, you'll need to change location='GITHUBUSERNAME/GITHUB-REPONAME#v to your repo.

With these in place, once you've committed your changes to your local repository, you can now do:

This will:

  • Set the package location to include the new version number

  • Publish to forgebox.io

  • Push your changes to gitHub (assuming you've set that up)

  • Publish a gitHub tagged release

This saves you having to manually update the version number too!

Lastly, you can double check it's made it into the plugins list via wheels plugins list

Removing a plugin from forgebox

Likewise, you can unpublish a plugin, but keep in mind people might be relying on your plugin, so don't do this lightly!

Database Migrations

Database Migrations are an easy way to build and alter your database structure using cfscript and even deploy across different database engines

With Wheels 2.x, you can now create, alter and populate your database via cfscript in an organized manner. Using custom CFC files, you can create an organized database schema, and move between versions easily, either programmatically, via the provided GUI, or via the CLI.

Getting Started

If you're new to this concept, the best way to get going is by following the [Migrator] link in the debug footer to load the built in GUI. Naturally, you will need your application's datasource setup and ready to go to get started.

You can go to the info tab in the navbar and you will see a Database section, just so you can check you're running against the correct datasource. We're going to start by creating a simple template.

Creating your First Template

The Templating tab allows for creation of either a blank CFC file, or from a selection of pre-populated templates. Whilst none of these templates will provide all the information required for a complete database migration, they are a good starting point and fairly heavily commented.

As we've not setup any migrations before, the system needs to know what prefix we want to use for our migration files. Each approach - Timestamp and Numeric is perfectly valid, but we recommend the Timestamp prefix if you're just starting out. Once you have a migration file, this section will disappear as it will get that info from the existing files.

For this tutorial, we're going to create the users table. So under Create a Template, we will select Create table and add a description of Create User Table.

Clicking on Create Migration File will then create a CFC at /app/migrator/migrations/20170420100502_Create_User_Table.cfc. The system will also display all messages at the bottom of the GUI whenever it does something - so for this command, we see The migration 20170420100502_Create_User_Table.cfc file was created

Populating the Create User Table Template

Next, open up the Create_User_Table.cfc template we just created. There are two functions to any migration file: up() and down().

up() will be executed when migrating your schema forward, and down() when you're rolling back.

The important concept to grasp is that anything which up() does, down() must undo.

Our default up() function will look something like this. Most of it you can actually ignore, as it's just wrapped in a transaction with some error handling. The important lines to look at are:

createTable() is the command to actually make the table: so we need to change this to users.

The t.create(); is the final statement which executes the actual action.

What goes up...

Remember, the down() function needs to reverse these changes. so in our down() code block, we're going to change the dropTable('tableName'); to `dropTable('users');

Adding additional columns

Whilst we could execute this template in it's current state (we have an up function which creates, and a down function which drops) we wouldn't get much in the actual table. We can use the same migration file to add additional lines to create some columns to store things like firstname. Here's an example of a slightly more fleshed out migration file to give you some inspiration:

As you can see, you can create multiple columns in a single call, set default values, whether to allow null values, and so on.

At this point, we can get going on actually creating this table

  • Make sure that multiple column names in "columnNames" are only separated with ",". Don't use spaces like ", " as that space becomes part of a column name which will cause problems.

Creating Tables with composite primary keys

While t = createTable(name='users'); will create a standard auto-increment numeric ID, sometimes you need to create a table which has a composite, or non standard primary key. In this example, we're setting id=false on the createTable() call to bypass the default behavior, then specifying our primarykeys separately via primaryKey():

This would be a typical setup for a join table where you have a many to many relationship. Alternatively this can be useful if you need to specify a UUID as a primarykey.

Running a migration

Returning to our migration GUI, we can now see some options under the Migrations tab.

Simply click the button to migrate the database to our new version. From this screen we can also roll back to previous schema versions, or even reset the database back to 0.

Migrator Configuration Settings

Setting Column Types

The Migrator needs to run across multiple DB engines, it avoids direct use of varchar, as different adapters will need to use different column types etc. Therefore string translates to VARCHAR.

For instance, here's the mySQL variants:

  • biginteger = BIGINT UNSIGNED

  • binary = BLOB boolean = TINYINT',limit=1

  • date = DATE datetime = DATETIME

  • decimal = DECIMAL

  • float = FLOAT

  • integer = INT

  • string = VARCHAR',limit=255

  • text = TEXT

  • time = TIME

  • timestamp = TIMESTAMP

  • uuid = VARBINARY', limit=16

Whereas SQL Server would use:

  • primaryKey = "int NOT NULL IDENTITY (1, 1)

  • binary = IMAGE

  • boolean = BIT

  • date = DATETIME

  • datetime = DATETIME

  • decimal = DECIMAL

  • float = FLOAT

  • integer = INT

  • string = VARCHAR',limit=255

  • text = TEXT

  • time = DATETIME

  • timestamp = DATETIME

  • uniqueidentifier = UNIQUEIDENTIFIER

  • char = CHAR',limit=10

Incorrect encoding example
Incorrect encoding example
Correct encoding
Correct encoding

In Wheels, one way to create objects that represent records in our table is by calling the class-level method.

At this point, the newAuthor object only exists in memory. We save it to the database by calling its method.

If you want to create a new object based on parameters sent in from a form request, the method conveniently accepts a struct as well. As we'll see later, when you use the Wheels form helpers, they automatically turn your form variables into a struct that you can pass into and other methods.

If you want to save a new author to the database right away, you can use the method instead.

Of course, if you do need to access the database default immediately after saving, Wheels allows this. Simply add reload=true to the , , or methods:

Sometimes a database default isn't the most appropriate solution because the value is only set after the model has been inserted. If you want to set a default value when it is first created with or , then you can pass the defaultValue argument of the method used in your model's config() block.

The built-in Wheels function will return a reference to an author object in the application scope (unless it's the first time you call this function, in which case it will also create and store it in the application scope).

Once you have the author object, you can start calling class methods on it, like , for example. returns an instance of the object with data from the database record defined by the key value that you pass.

Now authorObject is an instance of the Author class, and you can call object level methods on it, like and .

This code makes use of the class method , updates the object property in memory, and then saves it back to the database using the object method . We'll get back to all these methods and more later.

To change this behavior you can use the method. This method call should be placed in the config() method of your class file, which is where all configuration of your model is done.

With that in place, you have the foundation for a model that never touches the database. When you call methods like , , , and on a tableless model, the entire model lifecycle will still run, including object validation and object callbacks.

Typically, you will want to configure properties and validations manually for tableless models and then override and other persistence methods needed by your application to persist with the data elsewhere (maybe in the session scope, an external NoSQL database, or as an email sent from a contact form).

See for a worked-out example.

If you want to map a specific property to a column with a different name, you can override the Wheels mapping by using the method like this:

When you have created or retrieved an object, you can save it to the database by calling its method. This method returns true if the object passes all validations and the object was saved to the database. Otherwise, it returns false.

This chapter will focus on how to update records. Read the chapter for more information about how to create new records.

To cut down even more on lines of code, you can also combine the reading and saving of the objects by using the class-level methods and .

Give the method a primary key value (or several if you use composite keys) in the key argument, and it will update the corresponding record in your table with the properties you give it. You can pass in the properties either as named arguments or as a struct to the properties argument.

By default, will fetch the object first and call the update() method on it, thus invoking any callbacks and validations you have specified for the model. You can change this behavior by passing in instantiate=false. Then it will just update the record from the table using a simple UPDATE query.

An example of using by passing a struct:

And an example of using by passing named arguments:

The method allows you to update more than one record in a single call. You specify what records to update with the where argument and tell Wheels what updates to make using named arguments for the properties.

Unlike , the method will not instantiate the objects by default. That could be really slow if you wanted to update a lot of records at once.

Deleting records in Wheels is simple. If you have fetched an object, you can just call its method. If you don't have any callbacks specified for the class, all that will happen is that the record will be deleted from the table and true will be returned.

First, all methods registered to be run before a delete happens (these are registered using a call from the config function) will be executed, if any exist.

If the record was deleted, the callback code is executed, and whatever that code returns will be returned to you. (You should make all your callbacks return true or false.)

If you're unfamiliar with the concept of callbacks, you can read about them in the chapter.

There are also 3 class-level delete methods available: , , and . These work similarly to the class level methods for updating, which you can read more about in .

Display the links to all the other pages that the user should be able to go to. This is done using the function or using a lower-level function .

This chapter will deal with the first part: getting the paginated data. Please proceed to the chapter called if you wish to learn how to output the page links in your view.

Don't forget to check the chapter .

Well, good news. Of course you don't need to do this; just use the built-in functions , , , and .

Let's start with the function, shall we?

What if you only want to count authors with a last name starting with "A"? Like the function, will accept a where argument, so you can do this:

Just like in the function, you can now use the include argument to reference other tables.

OK, so now we've covered the function, but there are a few other functions you can use as well to get column statistics.

You can use the function to get the average value on any given column. The difference between this function and the function is that this operates on a single column, while the function operates on complete records. Therefore, you need to pass in the name of the property you want to get an average for.

To get the highest and lowest values for a property, you can use the and functions.

The last of the column statistics functions is the function.

As you have probably already figured out, adds all values for a given property and returns the result. You can use the same arguments as with the other functions (property, where, include, and distinct).

In the examples above, we've used the method, but you can use the same approach on a method as well.

In the background, these dynamically-named methods just pass along execution to or . This means that you can also pass in any arguments that are accepted by those two methods.

Reading records from your database typically involves using one of the 3 finder methods available in Wheels: , , and .

The first 2 of these, and , return an object, while the last one, findAll(), returns the result from a cfquery tag.

Let's start by looking at the simplest of the finder methods, . This method takes one argument: the primary key (or several keys if you're using composite keys) of the record you want to get.

You can use when you are asking to get one or more records from the database. Wheels will return this as a cfquery result (which could be empty if nothing was found based on your criteria).

Besides the difference in the default return type, and accept the same arguments. Let's have a closer look at these arguments.

This limits the number of records to return. Please note that if you call with maxRows=1, you will still get a cfquery result back and not an object. (We recommend using in this case if you want for an object to be returned.)

To do this, you use the returnAs argument. If you want an array of objects back from a call, for example, you can do this:

On and , you can set this argument to either object or query. On the method, you can set it to objects (note the plural) or query.

If your database table contains a field that is a foreign key to another table, then this is where to use the function.

The association is not used as often as the association, but it has its use cases. The most common use case is when you have a large table that you have broken down into two or more smaller tables (a.k.a. denormalization) for performance reasons or otherwise.

However, this is not a definite requirement. Wheels associations are completely independent of one another, so it's perfectly OK to setup a association without specifying the related association.

To join data from related tables in our calls, we simply need to use the include argument. Let's say that we wanted to include data about the author in our call for posts.

Let's say that you tell Wheels through a call that a post has many comments. What happens then is that Wheels will enrich the post model by adding a bunch of useful methods related to this association.

If you wanted to get all comments that have been submitted for a post, you can now call post.comments(). In the background, Wheels will delegate this to a call with the where argument set to postid=#post.id#.

Given that you have told Wheels that a post has many comments through a call, here are the methods that will be made available to you on the post model.

The association adds a few methods as well. Most of them are very similar to the ones added by .

Given that you have told Wheels that an author has one profile through a call, here are the methods that will be made available to you on the author model.

The association adds a couple of methods to your model as well.

Given that you have told Wheels that a comment belongs to a post through a call, here are the methods that will be made available to you on the comment model.

With the shortcut argument to , you can have Wheels create a dynamic method that lets you bypass the join model and instead reference the model on the other end of the many-to-many relationship directly.

For our example above, you can alter the call on the customer model to look like this instead:

This functionality relies on having set up all the appropriate and associations in all 3 models (like we have in our example in this chapter).

The through argument accepts a list of 2 association names. The first argument is the name of the association (set in the subscription model in this case), and the second argument is the association going back the other way (set in the publication model).

Nested properties in Wheels makes this scenario dead simple. With a configuration using the function in your model's config() method, you can save changes to that model and its associated models in a single call with , , or .

By adding the call to in the model, you can create both the user object and the profile object in a single call to .

In this example, we have added the addresses association to the call to .

Even with a complex form with a number of child objects, Wheels will save all of the data through its parent's , , or methods.

See the chapter on for more details.

Consider the many-to-many associations related to customers, publications, and subscriptions, straight from the chapter.

When it's time to save customers' subscriptions in the subscriptions join table, one approach is to loop through data submitted by s from your form, populate subscription model objects with the data, and call . This approach is valid, but it is quite cumbersome. Fortunately, there is a simpler way.

The main point of interest in this example is the <fieldset> for Subscriptions, which loops through the query of publications and uses the form helper. This helper is similar to and , but it is specifically designed for building form data related by associations. (Note that is primarily designed for columns in your table that store a single true/false value, so that is the big difference.)

Notice that the objectName argument passed to is the parent customer object and the associations argument contains the name of the related association. Wheels will build a form variable named in a way that the customer object is automatically bound to the subscriptions association.

All of these database queries will be wrapped in a . If any of the above updates don't pass validation or if the database queries fail, the transaction will roll back.

See the chapter about for more details.

One issue with ColdFusion is that you cannot nest <cftransaction> tags. In this case, Wheels provides a workaround. If you wish to run a method within a transaction, use , as below.

Wheels still allows for you to do this sort of dynamic calculation with the returnAs="objects" argument in methods like , but we advise against it when fetching large data sets because of the slowness of CreateObject() across CFML engines.

See the chapter on for more information.

With this line in place, fullName will become available in both full model objects and query objects returned by the various finder methods like and .

Fields set to NOT NULL will automatically trigger .

Numeric fields will automatically trigger .

Fields that have a maximum length will automatically trigger .

Automatic validations will not run for .

If your database column provides a default value for a given field, Wheels will not enforce a rule on that property.

You can also turn on or off the automatic validations on a per model basis by calling the method from a model's config() method.

See the chapter on for more information on available Wheels ORM settings.

At the end of the listing above are 3 custom validation functions: , , and . These functions allow you to create your own validation rules that aren't covered by Wheels's out-of-the-box functions.

runs on the save event, which happens on both create and update.

runs on create.

runs on update.

We then should create a method called validateEmailFormat, which in this case would verify that the value set for this.email is in the proper format. If not, then the method sets an error message for that field using the function.

We've mainly focused on adding error messages at a property level, which admittedly is what you'll be doing 80% of the time. But we can also add messages at the model object level with the function.

Now, to persist the object to the database, the model's call can be placed within a <cfif> test. If the save succeeds, the method will return true, and the contents of the <cfif> will be executed. But if any of the validations set up in the model fail, the method returns false, and the <cfelse> will execute.

The important step here is to recognize that the <cfelse> renders the original form input page using the function. When this happens, the view will use the newUser object defined in our method. If a were used instead, the validation information loaded in our method would be lost.

The biggest thing to note in this example is that a field called passwordConfirmation was provided so that the validation in the model can be properly tested.

For more information on how this code behaves when there is an error, refer to the chapter.

One area where this sense of losing control is especially noticeable is when you are using callbacks on objects (see the chapter on for more info). So let's use that for our example.

Here we are using the method to see if any of the object properties has changed.

When calling with no arguments, Wheels will check all properties on the object and return true if any of them have changed. If you want to see if a specific property has changed, you can pass in property="title" to it or use the dynamic method . Replace XXX with the name of the property. In our case, the method would then be named titleHasChanged().

If you want to see what a value was before a change was made, you can do so by calling and passing in the name of a property. This can also be done with the dynamic method.

When an object is in a changed state, there are a couple of methods you can use to report back on these changes. will give you a list of the property names that have been changed. returns a struct containing all the changes (both the property names and the changed values themselves).

If you have made changes to an object and for some reason you want to revert it back, you can do so by calling on it. This will query the database and update the object properties with their corresponding values from the database.

If you create a brand new object with the method and call on it, it will return true. The reason for this seemingly unexpected behavior is that change is always viewed from the database's perspective. The method will return true in this case because it is different from what is stored in the database (i.e. it doesn't exist at all in the database yet).

If you would simply like to know if an object exists in the database or not, you can use the method.

You create a new order object using the method based on the incoming form parameters.

You call the method on the order object, which will cause Wheels to first validate the object and then store it in the database if it passes validation.

The next day, you call the method on the object because the user decided to change the shipping method for the order.

Another day passes, and you call the method on the object because the visitor called in to cancel the order.

or

or

or

or

or

The very first possible callback that can take place in an object's life cycle is either or . The callback methods are triggered when you create the object yourself for the very first time, for example, when using the method. is triggered when the object is created as a result of fetching a record from the database, for example, when using . (There is some special behavior for this callback type that we'll explain in detail later on in this chapter).

If you want to completely break the callback chain for an object, you can do so by returning false from your callback method. (Otherwise, always return true or nothing at all.) As an example of breaking the callback chain, let's say you have called the method on a new object and the method you've registered with the callback returns false. As a result, because the method you've registered with the callback will exit the callback chain early by returning false, no record will be inserted in the database.

When you read about the callback above, you may have thought that it must surely only work for / calls but not for because those calls return query result sets by default, not objects.

Believe it or not, but callbacks are even triggered on ! You do need to write your callback code differently though because there will be no this scope in the query object. Instead of modifying properties in the this scope like you normally would, the properties are passed to the callback method via the arguments struct.

Because all callbacks run when fetching records from the database, it's a good idea to check to make sure that the columns used in the method's logic exist before performing any operations. You mostly encounter this issue when using the select argument on a finder to limit the number of columns returned. But no worries! You can use StructKeyExists() and perform a simple check to make sure that the columns exists in the arguments scope.

Please note that if you use the or the methods in Wheels, they will not instantiate objects by default, and thus any callbacks will be skipped. This is good for performance reasons because if you update 1,000 records at once, you probably don't want to run the callbacks on each object. Especially not if they involve database calls.

However, if you want to execute all callbacks in those methods as well, all you have to do is pass in instantiate=trueto the / methods.

To view all official plugins that are available for Wheels you can go to the listing on forgebox. Often the community will have a better idea of what plugins work best for your situation, so get on the mailing list and ask if you're in any doubt.

Wheels will use a createdat column automatically to store the current date and time when an INSERToperation is made (which could happen through a or operation, for example).

If Wheels sees an updatedat column, it will use it to store the current date and time automatically when an UPDATE operation is made (which could happen through a or operation, for example).

Wheels has built-in functionality for this so that you don't have to revert back to writing the queries and setting the data source manually whenever you need to use a data source other than the default one. In order accomplish this, you will use the function.

Overriding the default data source is done on a per model basis in Wheels by calling the function from within your model's config() method. By doing this, you instruct wheels to use that data source whenever it interacts with that model.

Because the photo model is the main model being used in the following example, its data source (myFirstDatabase) will be the one used in the query that ends up executing.

Let's say that we wanted Wheels's built-in function to return the time followed by the string " (approximately)":

See the chapter on for more details about changing Wheels settings.

With 2.x, a box.json is required for new plugins. Read the chapter for more details on that. One advantage is that Wheels now includes the version and meta data for each plugin when there's a box.json file.

So, you've created your new magic, world solving plugin, and naturally, you want to share it with the world. Wheels uses as a plugins repository. This enables us to manage our Wheels application's dependencies, install updates easily via CommandBox and more.

We strongly recommend always having the latest version of .

If you've not got a account you can either or very quickly via CommandBox itself

If this is the first time you've done this, you might want to try the forgebox staging server. That way you can make sure your publishing process is spot on without having lots of unnecessary versions pushed up. You can view the staging server version at

t.timestamps(); creates Wheels columns of createdAt,updatedAt and deletedAt.

Setting
Type
Default
Description
new()
save()
new()
new()
create()
create()
update()
save()
new()
create()
property()
model()
findByKey()
findByKey()
update()
save()
findByKey()
save()
table()
save()
create()
update()
delete()
save()
Building search forms with tableless models in Wheels
property()
save()
Creating Records
updateByKey()
updateAll()
updateByKey()
updateByKey()
updateByKey()
updateByKey()
updateAll()
updateByKey()
updateAll()
delete()
beforeDelete()
afterDelete()
Object Callbacks
deleteByKey()
deleteOne()
deleteAll()
Updating Records
paginationLinks()
pagination()
Displaying Links for Pagination
Displaying Links for Pagination
sum()
minimum()
maximum()
average()
count()
count()
findAll()
count()
findAll()
count()
average()
count()
count()
minimum()
maximum()
sum()
sum()
findOne()
findAll()
findOne()
findAll()
findByKey()
findOne()
findAll()
findByKey()
findOne()
findByKey()
findAll()
findOne()
findAll()
findAll()
findOne()
findAll()
findOne()
findByKey()
findAll()
belongsTo()
hasOne()
hasMany()
hasMany()
belongsTo()
findAll()
findAll()
hasMany()
findAll()
hasMany()
hasOne()
hasMany()
hasOne()
belongsTo()
belongsTo()
hasMany()
hasMany()
hasMany()
belongsTo()
belongsTo()
hasMany()
nestedProperties()
save()
create()
update()
nestedProperties()
create()
nestedProperties()
save()
update()
create()
Transactions
Associations
checkBoxTag()
save()
hasManyCheckBox()
checkBox()
checkBoxTag()
checkBox()
hasManyCheckBox()
Transaction
Configuration and Defaults
invokeWithTransaction()
findAll()
Reading Records
findAll()
findOne()
validatesConfirmationOf()
validatesExclusionOf()
validatesFormatOf()
validatesInclusionOf()
validatesLengthOf()
validatesNumericalityOf()
validatesPresenceOf()
validatesUniquenessOf()
validate()
validateOnCreate()
validateOnUpdate()
validatesPresenceOf()
validatesNumericalityOf()
validatesLengthOf()
Automatic Time Stamps
validatesPresenceOf()
automaticValidations()
Configuration and Defaults
validate()
validateOnCreate()
validateOnUpdate()
validate()
validateOnCreate()
validateOnUpdate()
addError()
addErrorToBase()
save()
save()
save()
renderView()
save()
redirectTo()
save()
validatesConfirmationOf()
Form Helpers and Showing Errors
Object Callbacks
hasChanged()
hasChanged()
XXXHasChanged()
changedFrom()
XXXChangedFrom()
changedProperties()
allChanges()
reload()
new()
hasChanged()
hasChanged()
isNew()
new()
save()
update(
delete()
afterNew()
afterFind()
afterInitialization()
beforeValidation()
beforeValidationOnCreate()
beforeValidationOnUpdate()
afterValidation()
afterValidationOnCreate()
afterValidationOnUpdate()
beforeSave()
beforeCreate()
beforeUpdate()
afterCreate()
afterUpdate()
afterSave()
beforeDelete()
afterDelete()
afterNew()
afterFind
afterNew()
new()
afterFind()
findByKey()
save()
beforeCreate()
beforeCreate()
afterFind()
findOne()
findByKey()
findAll()
findAll()
afterFind()
updateAll()
deleteAll()
updateAll()
deleteAll()
CommandBox
# List all Wheels plugins on forgebox
$ wheels plugins list
CommandBox
# install the Shortcodes plugin which has a slug of shortcodes
$ install shortcodes

# install the Select String plugin which has a slug of select-string
$ install select-string
component extends="Model" {

  function config(){
    dataSource("mySecondDatabase");
  }

}
app/models/Photo.cfc
component extends="Model" {

  function config(){
    dataSource("myFirstDatabase");
    hasMany("photoGalleries");
  }

}
app/models/PhotoGallery.cfc
component extends="Model" {

  function config(){
    dataSource("mySecondDatabase");
  }

}
FindAll Call
myPhotos = model("photo").findAll(include="photoGalleries");
ExamplePlugin.cfc
component {
  function init(){
    this.version="1.4.5,2.0";
    return this;
  }
}
timeAgoInWords.cfc
public string function timeAgoInWords(){
  return core.timeAgoInWords(argumentCollection=arguments) & " (approximately)";
}
<cfcomponent output="false" mixin="controller">
<cfcomponent output="false" mixin="controller" environment="development">
<cfcomponent output="false" dependency="someOtherPlugin">
app/config/development/settings.cfm
set(overwritePlugins=false);
app/config/development/settings.cfm
set(deletePluginDirectories=false);
pluginManager.installPlugin(URL.plugin);
ical4J
/**
 * Given a iCal style repeat rule, a seed date and a from-to range, get all recurring dates which satisfy those conditions
 *
 * [section: Plugins]
 * [category: Calendaring]
 *
 * @pattern The ical RRULE style string
 * @seed The seed date
 * @from When to generate repeat range from
 * @to When to generate repeat range till
 */
public array function getRecurringDates(
 required string pattern,
 required date seed,
 date from,
 date to
){
    local.recur = $ical_createRecur(arguments.pattern);
    return local.recur.getDates($ical_createDate(arguments.seed), $ical_createDate(arguments.from), $ical_createDate(arguments.to), $ical_createValue("DATE"));
}
// Version Number
application.wheels.pluginMeta["pluginName"]["version"];

// Meta Struct from box.json
application.wheels.pluginMeta["pluginName"]["boxjson"];
.travis.yml
language: java
sudo: required
jdk:
  - oraclejdk8
before_install:
  # Get Commandbox
  - sudo apt-key adv --keyserver keys.gnupg.net --recv 6DA70622
  - sudo echo "deb http://downloads.ortussolutions.com/debs/noarch /" | sudo tee -a /etc/apt/sources.list.d/commandbox.list
install:
  # Install Commandbox
  - sudo apt-get update && sudo apt-get --assume-yes install CommandBox
  # Check it's working
  - box version
  # Install CLI: needed to repackage the plugin to a zip on install
  - box install wheels-cli
  # Install Master Branch; nb, installed into folder of the git repo name, i.e neokoenig/wheels-ical4j
  - box install cfwheels/cfwheels
  # Install the Plugin: use gitHub path to get the absolute latest rather than the forgebox version
  - box install neokoenig/wheels-ical4j
before_script:
  # Master branch has a bunch of server.jsons we can use: lucee4 | lucee5 | cf10 | cf11 | cf2016
  - box server start lucee5
# Type should be the name of the plugin | servername should reflect the server we've just started
script: >
  testResults="$(box wheels test type=ical4j servername=lucee5)";
  echo "$testResults";
  if ! grep -i "\Tests Complete: All Good!" <<< $testResults;  then exit 1; fi
notifications:
    email: true
CommandBox
# Register for an account
$ forgebox register
CommandBox
# Login
$ forgebox login

# (optional) Check which account you're logged in with
$ forgebox whoami
Shell
# Create a basic box.json
$ init

# Or pass in parameters at the same time
$ init name="My Funky Plugin" slug=my-funky-plugin version=1.0.0 type="wheels-plugins"

# Or use the wizard
$ init --wizard
box.json
{
  // Required:
 "name":"PLUGIN-NAME",
 "version":"0.0.1",
 "author":"YOURNAME",
  // Required: GitHub Repository stub
 "location":"GITHUBUSERNAME/GITHUB-REPONAME#v0.0.1",
  // Required: Should always be /app/plugins/
 "directory":"/app/plugins/",
  // Required: Should always be true
 "createPackageDirectory":true,
  // Required: Must be the name of your primary CFC File
 "packageDirectory":"PLUGIN-NAME",
  // Required: The Forgebox slug, must be unique
 "slug":"FORGEBOX-SLUG",
  // Required: Must be wheels-plugins
 "type":"wheels-plugins",
  // Required: From here is optional but recommended
 "homepage":"https://github.com/GITHUBUSERNAME/GITHUB-REPONAME",
 "shortDescription":"PLUGIN DESCRIPTION",
 "keywords":"KEYWORD",
 "private":false,
 "scripts":{
   "postVersion":"package set location='GITHUBUSERNAME/GITHUB-REPONAME#v`package version`'",
 "patch-release":"bump --patch",
 "minor-release":"bump --minor",
 "major-release":"bump --major",
 "postPublish":"!git push --follow-tags && publish"
  }
}
box.json
{
  // Required:
 "name":"Shortcodes",
 "version":"0.0.4",
 "author":"Tom King",
  // Required: GitHub Repository stub, including version hash
 "location":"neokoenig/wheels-shortcodes#v0.0.4",
  // Required: Should always be /app/plugins/
 "directory":"/app/plugins/",
  // Required: Should always be true
 "createPackageDirectory":true,
  // Required: Must be the name of your primary CFC File
 "packageDirectory":"Shortcodes",
  // Required: The Forgebox slug, must be unique
 "slug":"shortcodes",
  // Required: Must be wheels-plugins
 "type":"wheels-plugins",
  // Required: From here is optional but recommended
 "homepage":"https://github.com/neokoenig/wheels-shortcodes",
 "shortDescription":"Shortcodes Plugin for Wheels",
 "keywords":"shortcodes",
 "private":false,
 "scripts":{
   "postVersion":"package set location='neokoenig/wheels-shortcodes#v`package version`'",
 "patch-release":"bump --patch",
 "minor-release":"bump --minor",
 "major-release":"bump --major",
 "postPublish":"!git push --follow-tags && publish"
  }
}
CommandBox
# Add staging server configuration
$ config set endpoints.forgebox.APIURL=http://forgebox.stg.ortussolutions.com/api/v1

# Revert back to production configuration
$ config clear endpoints.forgebox.APIURL
CommandBox
# from CommandBox prompt, within plugin directory
$ run-script patch-release
box.json
"scripts":{
   "postVersion":"package set location='GITHUBUSERNAME/GITHUB-REPONAME#v`package version`'",
 "patch-release":"bump --patch",
 "minor-release":"bump --minor",
 "major-release":"bump --major",
 "postPublish":"!git push --follow-tags && publish"
  }
CommandBox
# Don't forget to commit your changes. You can access git directly from commandbox using !
$ !git add .
$ !git commit -m "my new changes"

# Move from 1.0.0 -> 1.0.1
$ run-script patch-release

# Move from 1.0.0 -> 1.1.0
$ run-script minor-release

# Move from 1.0.0 -> 2.0.0
$ run-script major-release
Commandbox
# Example output of a patch release
$ run-script patch-release

Running package script [patch-release].
> bump --patch && publish
Set version = 0.1.7

Running package script [postVersion].
> package set location='neokoenig/wheels-cli#v`package version`'
Set location = neokoenig/wheels-cli#v0.1.7
Package is a Git repo.  Tagging...
Tag [v0.1.7] created.
Sending package information to ForgeBox, please wait...
Package is alive, you can visit it here: https://www.forgebox.io/view/wheels-cli

Running package script [postPublish].
> !git push --follow-tags
Counting objects: 10, done.
Delta compression using up to 12 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (10/10), 908 bytes | 0 bytes/s, done.
Total 10 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 4 local objects.
To https://github.com/neokoenig/wheels-cli.git
   477648f..400604b  master -> master
 * [new tag]         v0.1.7 -> v0.1.7

Package published successfully in [forgebox]
CommandBox
// Remove all versions of a package
$ unpublish

// Remove a specific version of a package
$ unpublish 1.2.3

// Skip the user confirmation prompt
$ unpublish 1.2.3 --force
up()
function up() {
	transaction {
		try {
			t = createTable(name = 'tableName');
			t.timestamps();
			t.create();
		} catch (any e) {
			local.exception = e;
		}

		if (StructKeyExists(local, "exception")) {
			transaction action="rollback";
			Throw(errorCode = "1", detail = local.exception.detail, message = local.exception.message, type = "any");
		} else {
			transaction action="commit";
		}
	}
}
up()
t = createTable(name='tableName');
t.timestamps();
t.create();
up()
t = createTable(name='users');
t.timestamps();
t.create();
up()
t = createTable(name='users');
t.string(
 columnNames='firstname,lastname,password',
 default='', null=false, limit='60');

t.string(
 columnNames='username,passwordresettoken,apikey',
 default='', null=true, limit='60');

t.string(
 columnNames='email,address1,address2,city,county,country,tel,www',
 default='', null=true, limit='255');

t.string(
 columnNames='title,postcode,lang,locale,timezone',
 default='', null=true, limit='15');

t.integer(columnNames='roleid', default='0', null=false, limit='11');
t.datetime(columnNames='pwresettokenat', default='', null=true);
t.datetime(columnNames='pwlastresetat', default='', null=true);

t.timestamps();
t.create();
t = createTable(name='rolepermissions', id=false);
t.primaryKey(name="roleid", null=false, limit=11);
t.primaryKey(name="permissionid", null=false, limit=11);
t.create();

autoMigrateDatabase

Boolean

false

Automatically runs available migration on applicationstart.

migratorTableName

String

migratorversions

The name of the table that stores the versions migrated.

createMigratorTable

Boolean

true

Create the migratorversions database table.

writeMigratorSQLFiles

Boolean

false

Writes the executed SQL to a .sql file in the app/migrator/sql directory.

migratorObjectCase

String

lower

Specifies the case of created database object. Options are 'lower', 'upper' and 'none' (which uses the given value unmodified)

allowMigrationDown

Boolean

false (true in development mode)

Prevents 'down' migrations (rollbacks)

Plugins
save()
create()
save()
update()
dataSource()
dataSource()
findAll()
timeAgoInWords()
Configuration and Defaults
Publishing Plugins
forgebox.io
CommandBox
forgebox.io
register directly on forgebox
http://forgebox.stg.ortussolutions.com/
automatic timestamp

Overview

The intention of this section is to document the various pieces of the tooling that make the project function. This is not so much about the framework and using the framework as a developer but more for collaborator, contributors, and maintainers of the project.

Wheels Guides

Making changes to the Guides

API Documentation

So the first step in submitting changes to the API Documentation is similar to the Wheels Guides and starts with cloning the repository, making the changes, and submitting a PR.

Once approved and merged in, then the json file is generated using a utility embedded in the framework itself. So once a version has been published to ForgeBox (either a SNAPSHOT or a stable release), use command box to install that version of the framework.

wheels generate app name=json template=wheels-base-template@be cfmlEngine=lucee@5

This will install the Bleeding Edge version of the framework with the Lucee v5 CFML Engine. Once the installation completes start your server.

server start

When the Congratulations screen is displayed, click on the Info Tab on the top menu and then the Utils tab from the sub menu.

Then click on the Export Docs as JSON link to generate the json file. Save the json file. Typically these files are named based on the version of the framework they represent. i.e. the file for v2.5.0 would be named v2.5.json.

At the moment the process of updating the API Documentation is very manual, I hope to be able to extend the CI pipeline and automatically update the API docs with each commit similarly to how the packages on ForgeBox are published automatically on each commit.

The guides are hosted by and are accessible at they are however driven by the folder in the Wheels repository on GitHub. This means that making changes to the guides can be accomplished via a Pull Request and code changes and changes to the guides can be submitted in the same PR.

Start by cloning the repository. The guides are contained in the guides folder of the repo. Make your proposed changes and submit a PR. Once the PR is reviewed and merged, the changes will automatically be synced up to GitBook and the changes will be live on the site.

The API Documentation is comprised of two parts. The first is a json file that contains the data for a particular version of the framework and the second is a small Wheels App that reads that json file and displays the UI you see when you visit .

We use a javadoc style of notation to document all the public functions of the framework. This system is actually available to you to document your own functions and is documented at . Additionally the sample code is driven off text files that are located in .

Now that we have generated the JSON data file, we need to add it to the codebase for the API Documentation site. This codebase is driven from the . A PR can be used to submit the json file to this repository. Currently the core team is manually adding this file to the repository when the API docs need to be updated.

The application is then uploaded to the site hosted by .

GitBooks.com
https://guides.wheels.dev
guides
Wheels
https://api.wheels.dev
Documenting your Code
wheels/public/docs/reference
wheels-api repository
Viviotech
Figure 1: Wheels congratulations screen
wheels new - step 1
wheels new - step 2
wheels new - step 3
wheels new - step 4
wheels new - step 5
wheels new - step 6
wheels new - step 7
wheels new - step 8
wheels new - final screen

Soft Delete

An easy way to keep deleted data in your database.

"Soft delete" in database lingo means that you set a flag on an existing table which indicates that a record has been deleted, instead of actually deleting the record.

How to Use Soft Deletion

If you create a new date column (the column type will depend on your database vendor, but usually you want to use date, datetime, or timestamp) on a table and name it deletedAt, Wheels will automagically start using it to record soft deletes.

Without the soft delete in place, a delete() call on an object will delete the record from the table using a DELETE statement. With the soft delete in place, an UPDATE statement is sent instead (that sets the deletedAt field to the current time).

Of course, all other Wheels functions are smart enough to respect this. So if you use a findAll() function, for example, it will not return any record that has a value set in the deletedAt field.

What this all means is that you're given a convenient way to keep deleted data in your database forever, while having your application function as if the data is not there.

Getting data including Soft Deletes

Occasionally you might want to include data which has been flagged for deletion. You can do this easily by adding includeSoftDeletes=true to any findAll type call.

Obviously, if you have any manual queries in your application, you'll need to remember to add deletedAt IS NULL to the WHERE part of your SQL statements instead.