Skip to the content.

Developing Applications with Ziggy MVC

This guide covers everything you need to know to build robust applications with Ziggy MVC.

Application Structure

A Ziggy MVC application follows this conventional structure:

/myapp
  /controllers          # Request handlers
  /layouts              # Layout templates
  /model                # Business logic
    /beans              # Domain objects (transients)
    /services           # Service layer (singletons)
  /views                # View templates
    /section            # Views for each section
  /subsystems           # Optional: modular subsystems
  Application.cfc       # Application configuration
  index.cfm             # Entry point (usually empty)

Alternative Structure

If you prefer different folder names, configure them:

variables.framework = {
    controllersFolder = "handlers",
    viewsFolder = "pages",
    layoutsFolder = "wrappers"
};

Views and Layouts

Views

Views are CFM templates that render the response. They receive:

Example view (views/user/profile.cfm):

<cfoutput>
<div class="profile">
    <h2>#rc.user.getName()#</h2>
    <p>Email: #rc.user.getEmail()#</p>
    <p>Member since: #dateFormat(rc.user.getCreatedDate(), "mmmm d, yyyy")#</p>
</div>
</cfoutput>

Using the local Scope

Always use local for temporary variables in views to avoid conflicts:

<cfset local.formattedDate = dateFormat(rc.createdAt, "yyyy-mm-dd")>
<cfoutput>#local.formattedDate#</cfoutput>

Layouts

Layouts wrap views with common HTML structure. They receive a body variable containing the rendered view.

Example layout (layouts/default.cfm):

<!DOCTYPE html>
<html>
<head>
    <title><cfoutput>#rc.pageTitle ?: "My Application"#</cfoutput></title>
</head>
<body>
    <nav>
        <cfoutput>
        <a href="#buildURL('main.default')#">Home</a>
        <a href="#buildURL('user.list')#">Users</a>
        </cfoutput>
    </nav>

    <main>
        <cfoutput>#body#</cfoutput>
    </main>
</body>
</html>

Layout Cascade

For action section.item, Ziggy MVC searches for layouts in order:

  1. layouts/section/item.cfm (view-specific)
  2. layouts/section.cfm (section-specific)
  3. layouts/default.cfm (site-wide)

Call disableLayout() in a controller to stop the cascade.

Rendering Other Views

Include another view within a view using the view() function:

<cfoutput>
<div class="sidebar">
    #view('widgets/recentPosts')#
</div>
<div class="main">
    #view('user/profile')#
</div>
</cfoutput>

Controllers

Controllers handle requests and prepare data for views. Each controller is a CFC in the /controllers folder.

Basic Controller

component accessors="true" {

    // Services are automatically injected
    property userService;

    function list(struct rc) {
        rc.users = userService.getAllUsers();
    }

    function view(struct rc) {
        param name="rc.id" default="0";
        rc.user = userService.getUserById(rc.id);
        if (isNull(rc.user)) {
            rc.message = "User not found";
            setView("main.error");
        }
    }

    function save(struct rc) {
        var user = userService.save(rc);
        redirect("user.view", "id", user.getId());
    }

}

Controller Lifecycle

For each request, Ziggy MVC calls methods in this order:

  1. Application.cfc : before(rc)
  2. controller : before(rc)
  3. controller : item(rc) (the requested action)
  4. controller : after(rc)
  5. Application.cfc : after(rc)

Before and After Methods

Use before() and after() for cross-cutting concerns:

component accessors="true" {

    property securityService;

    function before(struct rc) {
        // Run before every action in this controller
        if (!securityService.isAuthenticated()) {
            redirect("login.default");
        }
    }

    function list(struct rc) {
        // Main action logic
    }

    function after(struct rc) {
        // Run after every action (e.g., logging)
    }

}

Accessing the Framework

Declare the framework as a property or receive it in init():

Using property:

component accessors="true" {
    property framework;

    function doSomething(struct rc) {
        framework.redirect("user.list");
    }
}

Using init():

component {
    function init(fw) {
        variables.fw = fw;
        return this;
    }

    function doSomething(struct rc) {
        variables.fw.redirect("user.list");
    }
}

Handling Missing Methods

Override onMissingMethod() for dynamic actions:

function onMissingMethod(string missingMethodName, struct missingMethodArguments) {
    var rc = missingMethodArguments.rc;
    var method = missingMethodArguments.method; // "before", "item", or "after"

    if (method == "item") {
        rc.message = "Action '#missingMethodName#' not found";
        setView("main.error");
    }
}

Services and Domain Objects

Service Layer

Services contain business logic and are singletons by default. Place them in /model/services/:

// model/services/userService.cfc
component {

    property userDAO;
    property emailService;

    function init() {
        return this;
    }

    function getAllUsers() {
        return userDAO.findAll();
    }

    function createUser(required string email, required string name) {
        var user = new beans.user();
        user.setEmail(email);
        user.setName(name);

        userDAO.save(user);
        emailService.sendWelcome(user);

        return user;
    }

}

Domain Objects (Beans)

Domain objects are transients (new instance each time). Place them in /model/beans/:

// model/beans/user.cfc
component accessors="true" {

    property name="id" type="numeric";
    property name="email" type="string";
    property name="name" type="string";
    property name="createdAt" type="date";

    function init() {
        variables.createdAt = now();
        return this;
    }

    function getDisplayName() {
        return len(variables.name) ? variables.name : variables.email;
    }

}

Populating Objects

Use populate() to map request context to object properties:

function save(struct rc) {
    var user = getBeanFactory().getBean("user");

    // Populate all matching properties
    populate(user);

    // Or populate specific properties only
    populate(user, "email,name,phone");

    // With options
    populate(cfc=user, keys="email,name", trim=true, trustKeys=true);

    userService.save(user);
}

Configuration

Configure Ziggy MVC via variables.framework in Application.cfc:

variables.framework = {
    // Action parameter name
    action = "action",

    // Default action when none specified
    home = "main.default",

    // Error handler action
    error = "main.error",

    // Reload settings
    reload = "reload",
    password = "secret",

    // URL generation
    generateSES = false,
    SESOmitIndex = false,

    // Dependency injection
    diEngine = "di1",
    diLocations = "model,controllers",
    diConfig = {},

    // Development options
    reloadApplicationOnEveryRequest = false,
    trace = false,

    // Folder customization
    controllersFolder = "controllers",
    viewsFolder = "views",
    layoutsFolder = "layouts",

    // Subsystems (if used)
    usingSubsystems = false,
    defaultSubsystem = "home",
    subsystemDelimiter = ":",

    // Request handling
    unhandledExtensions = "cfc",
    unhandledPaths = "/flex2gateway",

    // Routes
    routes = [],
    routesCaseSensitive = true
};

Key Configuration Options

Setting Default Description
action “action” URL/form variable name for the action
home “main.default” Default action when none specified
error “main.error” Error handler action
generateSES false Generate SEO-friendly URLs
diEngine “di1” Dependency injection engine
diLocations “model,controllers” Folders to scan for beans
trace false Enable framework tracing

URL Routes

Define custom URL routes for clean URLs:

variables.framework.routes = [
    // Simple routes
    { "/products" = "/product/list" },
    { "/products/:id" = "/product/view/id/:id" },

    // HTTP method-specific routes
    { "$GET/api/users" = "/api/user/list" },
    { "$POST/api/users" = "/api/user/create" },
    { "$PUT/api/users/:id" = "/api/user/update/id/:id" },
    { "$DELETE/api/users/:id" = "/api/user/delete/id/:id" },

    // Redirect routes
    { "/old-page" = "301:/new-page" },

    // Wildcard (must be last)
    { "*" = "/main/notfound" }
];

Route Placeholders

Use :name for simple placeholders or {name:[regex]} for regex patterns:

{ "/user/:id" = "/user/view/id/:id" },
{ "/user/{id:[0-9]+}" = "/user/view/id/:id" },
{ "/post/{slug:[a-z0-9-]+}" = "/blog/post/slug/:slug" }

Resource Routes

Auto-generate RESTful routes:

variables.framework.routes = [
    { "$RESOURCES" = "users,posts,comments" }
];

This generates standard REST endpoints:

Rendering Data (REST APIs)

Return JSON, XML, or other formats using renderData():

function apiUsers(struct rc) {
    var users = userService.getAllUsers();

    // Return JSON
    renderData().data(users).type("json");
}

function apiUser(struct rc) {
    var user = userService.getUserById(rc.id);

    if (isNull(user)) {
        renderData().data({error: "User not found"}).type("json").statusCode(404);
    } else {
        renderData().data(user).type("json");
    }
}

Supported Types

Custom Renderers

Define custom rendering functions:

function render_csv(struct renderData) {
    return {
        contentType = "text/csv",
        output = convertToCSV(renderData.data)
    };
}

// Usage
renderData().data(users).type(render_csv);

Error Handling

Error Action

Configure a default error handler:

variables.framework.error = "main.error";

Create the error view (views/main/error.cfm):

<cfoutput>
<h1>An Error Occurred</h1>

<cfif structKeyExists(request, "exception")>
    <p>#request.exception.message#</p>

    <cfif structKeyExists(request, "failedAction")>
        <p>Failed action: #request.failedAction#</p>
    </cfif>
</cfif>

<p><a href="#buildURL('main.default')#">Return Home</a></p>
</cfoutput>

Custom Error Handling

Override onError() in Application.cfc:

function onError(exception, event) {
    // Log the error
    writeLog(file="errors", text=exception.message);

    // Set up error data
    request.exception = exception;
    request.event = event;

    // Call parent error handling
    super.onError(argumentCollection=arguments);
}

Environment Support

Configure different settings per environment:

function getEnvironment() {
    if (findNoCase("localhost", CGI.SERVER_NAME)) return "development";
    if (findNoCase("staging", CGI.SERVER_NAME)) return "staging";
    return "production";
}

variables.framework.environments = {
    development = {
        reloadApplicationOnEveryRequest = true,
        trace = true
    },
    staging = {
        trace = true
    },
    production = {
        password = "supersecretpassword"
    }
};

function setupEnvironment(string env) {
    // Environment-specific initialization
    if (env == "production") {
        // Set production database, caching, etc.
    }
}

Application Lifecycle

Override these methods in Application.cfc:

setupApplication()

Called when the application starts or reloads:

function setupApplication() {
    // Initialize application-wide resources
    application.startTime = now();
}

setupSession()

Called when a new session starts:

function setupSession() {
    // Initialize session data
    session.cart = [];
}

setupRequest()

Called at the start of each request:

function setupRequest() {
    // Queue additional controllers
    controller("security.checkAuth");

    // Set up request-scoped data
    request.startTime = getTickCount();
}

setupView()

Called after controllers run, before view renders:

function setupView(struct rc) {
    // Set up data needed by all views
    rc.currentYear = year(now());
}

setupResponse()

Called at the end of every request:

function setupResponse(struct rc) {
    // Clean up, logging, etc.
    var elapsed = getTickCount() - request.startTime;
    writeLog(file="perf", text="Request completed in #elapsed#ms");
}

Session Management

Ziggy MVC provides secure session helper methods:

// Write to session
sessionWrite("userId", 123);

// Read from session
var userId = sessionRead("userId");

// Read with default
var cart = sessionRead("cart", []);

// Check if exists
if (sessionHas("userId")) { ... }

// Delete from session
sessionDelete("userId");

// Execute within session lock
sessionLock(function() {
    // Thread-safe session operations
    session.counter++;
});

Building URLs

Use buildURL() for framework-aware URL generation:

<!--- Simple action --->
<a href="#buildURL('user.list')#">Users</a>

<!--- With parameters --->
<a href="#buildURL('user.view', 'id=#user.getId()#')#">View</a>

<!--- With struct parameters --->
<a href="#buildURL(action='user.edit', queryString={id: user.getId(), mode: 'full'})#">Edit</a>

buildCustomURL()

For route-based URLs:

<a href="#buildCustomURL('/users/' & user.getId())#">View User</a>

Redirects

Redirect to another action:

// Simple redirect
redirect("user.list");

// With flash data preservation
redirect(action="user.view", preserve="message", append="id");

// Preserve all request context
redirect(action="user.view", preserve="all");

redirectCustomURL()

For custom URL redirects:

redirectCustomURL("/users/" & user.getId());

Tracing and Debugging

Enable tracing for debugging:

variables.framework.trace = true;

Add custom trace messages:

frameworkTrace("Processing user #rc.id#");

View trace output appended to responses during development.