Wheels Controller Generator

Generate Wheels MVC controllers with CRUD actions, filters, parameter verification, and proper rendering. Use when creating or modifying controllers, adding actions, implementing filters for authentication/authorization, handling form submissions, or rendering views/JSON. Ensures proper Wheels conventions and prevents common controller errors.

$ 설치

git clone https://github.com/wheels-dev/wheels /tmp/wheels && cp -r /tmp/wheels/.claude/skills/wheels-controller-generator ~/.claude/skills/wheels

// tip: Run this command in your terminal to install the skill


name: Wheels Controller Generator description: Generate Wheels MVC controllers with CRUD actions, filters, parameter verification, and proper rendering. Use when creating or modifying controllers, adding actions, implementing filters for authentication/authorization, handling form submissions, or rendering views/JSON. Ensures proper Wheels conventions and prevents common controller errors.

Wheels Controller Generator

When to Use This Skill

Activate automatically when:

  • User requests to create a new controller (e.g., "create a Users controller")
  • User wants to add CRUD actions (index, show, new, create, edit, update, delete)
  • User needs filters (beforeAction/afterAction)
  • User wants authentication or authorization
  • User is implementing API endpoints
  • User mentions: controller, action, filter, CRUD, API, JSON, render, redirect

Critical Patterns

✅ CORRECT Controller Structure

component extends="Controller" {

    function config() {
        // Parameter verification
        verifies(only="show,edit,update,delete", params="key", paramsTypes="integer");

        // Filters
        filters(through="findResource", only="show,edit,update,delete");
        filters(through="requireAuth", except="index,show");
    }

    // Public action methods
    function index() {
        resources = model("Resource").findAll(order="createdAt DESC");
    }

    // Private filter methods
    private function findResource() {
        resource = model("Resource").findByKey(key=params.key);
        if (!isObject(resource)) {
            flashInsert(error="Resource not found");
            redirectTo(action="index");
        }
    }
}

❌ ANTI-PATTERNS to Avoid

Don't mix argument styles:

// ❌ WRONG
resource = model("Resource").findByKey(params.key, include="comments");

// ✅ CORRECT
resource = model("Resource").findByKey(key=params.key, include="comments");

Don't forget parameter verification:

// ❌ WRONG - No verification, vulnerable to injection
function show() {
    post = model("Post").findByKey(key=params.key);
}

// ✅ CORRECT - Verify params before use
function config() {
    verifies(only="show", params="key", paramsTypes="integer");
}

Don't forget CSRF protection in forms:

// ❌ WRONG - Forms without CSRF
#startFormTag(action="create")#

// ✅ CORRECT - CSRF token included by default
#startFormTag(action="create")#  // Wheels adds CSRF automatically

CRITICAL: Filter must run for ALL actions that use loaded data:

// ❌ WRONG - Filter doesn't run for show, but show expects 'user' to be loaded
function config() {
    filters(through="findUser", only="edit,update,delete");
}
function show() {
    // ERROR: user variable not defined!
    renderView();
}

// ✅ CORRECT - Filter runs for ALL actions that need the data
function config() {
    filters(through="findUser", only="show,edit,update,delete");
}
function show() {
    // user variable loaded by filter
    renderView();
}

CRITICAL: Centralize key parameter resolution in filters:

// ❌ WRONG - Duplicated logic in multiple actions
function show() {
    if (!structKeyExists(params, "key") && structKeyExists(session, "userId")) {
        params.key = session.userId;
    }
    user = model("User").findByKey(key=params.key);
}

// ✅ CORRECT - Centralized in filter
private function findUser() {
    // Handle session userId fallback
    if (!structKeyExists(params, "key") && structKeyExists(session, "userId")) {
        params.key = session.userId;
    }
    user = model("User").findByKey(key=params.key);
    if (!isObject(user)) {
        flashInsert(error="User not found");
        redirectTo(controller="home", action="index");
    }
}

CRUD Controller Template

Complete CRUD Implementation

component extends="Controller" {

    function config() {
        // Verify key parameter is integer
        verifies(only="show,edit,update,delete", params="key", paramsTypes="integer");

        // Load resource for actions that need it
        filters(through="findResource", only="show,edit,update,delete");
    }

    /**
     * List all resources
     */
    function index() {
        resources = model("Resource").findAll(
            order="createdAt DESC",
            include="associations",  // Prevent N+1 queries
            page=params.page
        );
    }

    /**
     * Show single resource
     */
    function show() {
        // Resource loaded by filter
        // Load associated data
        associations = resource.associations(order="createdAt ASC");
    }

    /**
     * New resource form
     */
    function new() {
        resource = model("Resource").new();
    }

    /**
     * Create new resource
     */
    function create() {
        resource = model("Resource").new(params.resource);

        if (resource.save()) {
            flashInsert(success="Resource created successfully!");
            redirectTo(action="show", key=resource.key());
        } else {
            flashInsert(error="Please correct the errors below.");
            renderView(action="new");
        }
    }

    /**
     * Edit resource form
     */
    function edit() {
        // Resource loaded by filter
    }

    /**
     * Update resource
     */
    function update() {
        // Resource loaded by filter

        if (resource.update(params.resource)) {
            flashInsert(success="Resource updated successfully!");
            redirectTo(action="show", key=resource.key());
        } else {
            flashInsert(error="Please correct the errors below.");
            renderView(action="edit");
        }
    }

    /**
     * Update with optional password change (Task 4 pattern)
     * Use this for user profile updates where password change is optional
     */
    function updateWithOptionalPassword() {
        // User loaded by filter

        // Handle optional password change - if blank, don't change it
        if (structKeyExists(params.user, "password")) {
            if (!len(trim(params.user.password))) {
                structDelete(params.user, "password");
                structDelete(params.user, "passwordConfirmation");
            }
        }

        if (user.update(params.user)) {
            flashInsert(success="Profile updated successfully!");
            redirectTo(action="show", key=user.key());
        } else {
            flashInsert(error="Please correct the errors below.");
            renderView(action="edit");
        }
    }

    /**
     * Delete resource
     */
    function delete() {
        // Resource loaded by filter

        if (resource.delete()) {
            flashInsert(success="Resource deleted successfully!");
            redirectTo(action="index");
        } else {
            flashInsert(error="Unable to delete resource.");
            redirectTo(action="show", key=resource.key());
        }
    }

    /**
     * Private filter to load resource
     */
    private function findResource() {
        resource = model("Resource").findByKey(key=params.key);

        if (!isObject(resource)) {
            flashInsert(error="Resource not found.");
            redirectTo(action="index");
        }
    }
}

Filter Patterns

Authentication Filter

component extends="Controller" {

    function config() {
        // Require authentication for all actions except index and show
        filters(through="requireAuth", except="index,show");
    }

    private function requireAuth() {
        if (!structKeyExists(session, "userId")) {
            flashInsert(error="Please log in to continue.");
            redirectTo(controller="sessions", action="new");
        }
    }
}

Authorization Filter

component extends="Controller" {

    function config() {
        filters(through="requireAuth");
        filters(through="requireOwnership", only="edit,update,delete");
    }

    private function requireAuth() {
        if (!structKeyExists(session, "userId")) {
            flashInsert(error="Please log in.");
            redirectTo(controller="sessions", action="new");
        }
    }

    private function requireOwnership() {
        resource = model("Resource").findByKey(key=params.key);

        if (!isObject(resource) || resource.userId != session.userId) {
            flashInsert(error="You don't have permission to access this resource.");
            redirectTo(action="index");
        }
    }
}

Data Loading Filter

component extends="Controller" {

    function config() {
        // Load current user for all actions
        filters(through="loadCurrentUser");
    }

    private function loadCurrentUser() {
        if (structKeyExists(session, "userId")) {
            currentUser = model("User").findByKey(key=session.userId);
        }
    }
}

Rendering Patterns

Render View

function index() {
    resources = model("Resource").findAll();
    // Automatically renders views/resources/index.cfm
}

Render Specific View

function create() {
    resource = model("Resource").new(params.resource);

    if (!resource.save()) {
        // Render the new action's view
        renderView(action="new");
    }
}

Render JSON (API)

function index() {
    resources = model("Resource").findAll();

    renderWith(
        data=resources,
        format="json",
        status=200
    );
}

Render Partial

function loadMore() {
    resources = model("Resource").findAll(page=params.page);
    renderPartial(partial="resource", collection=resources);
}

Send File

function download() {
    resource = model("Resource").findByKey(key=params.key);

    sendFile(
        file=resource.filePath,
        name=resource.fileName,
        disposition="attachment"
    );
}

Redirect Patterns

Redirect to Action

function create() {
    resource = model("Resource").new(params.resource);

    if (resource.save()) {
        redirectTo(action="show", key=resource.key());
    }
}

Redirect to Controller/Action

function logout() {
    structDelete(session, "userId");
    redirectTo(controller="home", action="index");
}

Redirect to URL

function external() {
    redirectTo(url="https://wheels.dev");
}

Redirect Back

function cancel() {
    redirectTo(back=true, default="index");
}

Flash Message Patterns

Success Messages

flashInsert(success="Operation completed successfully!");

Error Messages

flashInsert(error="An error occurred. Please try again.");

Multiple Message Types

flashInsert(
    success="Resource created!",
    notice="Check your email for confirmation."
);

Flash Keep (preserve across redirect chain)

flashKeep("success");
redirectTo(action="intermediate");

Parameter Handling

Parameter Verification

function config() {
    // Verify key is integer
    verifies(only="show", params="key", paramsTypes="integer");

    // Verify multiple params
    verifies(only="create", params="name,email", paramsTypes="string,string");

    // Verify with default values
    verifies(params="page", default=1, paramsTypes="integer");
}

Safe Parameter Access

function index() {
    // Use params with defaults
    page = structKeyExists(params, "page") ? params.page : 1;

    // Or let Wheels handle it with verifies()
    resources = model("Resource").findAll(page=params.page);
}

Nested Parameters (Forms)

function create() {
    // Form submits: user[name], user[email], user[password]
    // Wheels creates: params.user = {name="", email="", password=""}

    user = model("User").new(params.user);
}

API Controller Patterns

JSON API Controller

component extends="Controller" {

    function config() {
        // Set default rendering to JSON
        provides("json");

        // Verify API authentication
        filters(through="requireApiAuth");
    }

    function index() {
        resources = model("Resource").findAll();

        renderWith(
            data=resources,
            format="json",
            status=200
        );
    }

    function show() {
        resource = model("Resource").findByKey(key=params.key);

        if (!isObject(resource)) {
            renderWith(
                data={error="Resource not found"},
                format="json",
                status=404
            );
            return;
        }

        renderWith(
            data=resource,
            format="json",
            status=200
        );
    }

    function create() {
        resource = model("Resource").new(params.resource);

        if (resource.save()) {
            renderWith(
                data=resource,
                format="json",
                status=201,
                location=urlFor(action="show", key=resource.key())
            );
        } else {
            renderWith(
                data={errors=resource.allErrors()},
                format="json",
                status=422
            );
        }
    }

    private function requireApiAuth() {
        var authHeader = getHTTPRequestData().headers["Authorization"];

        if (!structKeyExists(local, "authHeader") || !isValidToken(authHeader)) {
            renderWith(
                data={error="Unauthorized"},
                format="json",
                status=401
            );
            abort;
        }
    }
}

Nested Resource Controllers

Nested Resource Pattern

// URL: /posts/5/comments
component extends="Controller" {

    function config() {
        verifies(params="postId", paramsTypes="integer");
        filters(through="loadPost");
    }

    function index() {
        // Post loaded by filter
        comments = post.comments(order="createdAt DESC");
    }

    function create() {
        comment = model("Comment").new(params.comment);
        comment.postId = params.postId;

        if (comment.save()) {
            flashInsert(success="Comment added!");
            redirectTo(controller="posts", action="show", key=params.postId);
        }
    }

    private function loadPost() {
        post = model("Post").findByKey(key=params.postId);

        if (!isObject(post)) {
            flashInsert(error="Post not found.");
            redirectTo(controller="posts", action="index");
        }
    }
}

Implementation Checklist

When generating a controller:

  • Component extends="Controller"
  • config() function defined
  • Parameter verification configured with verifies()
  • Filters defined as private functions
  • All model calls use named parameters
  • Flash messages for user feedback
  • Proper redirects after POST/PUT/DELETE
  • Error handling for not found resources
  • CRUD actions follow conventions
  • Public action methods, private filter methods

Testing Controllers

// Test controller instantiation
controller = controller("Resources");

// Test action execution
controller.processAction("index");

// Test filter execution
controller.processAction("show", {key=1});

// Check variables set by action
expect(controller.resources).toBeQuery();

Related Skills

  • wheels-anti-pattern-detector: Validates controller code
  • wheels-view-generator: Creates views for controller actions
  • wheels-test-generator: Creates controller specs
  • wheels-model-generator: Creates models used by controller

Quick Reference

Common Controller Methods

  • renderView() - Render specific view
  • renderPartial() - Render partial
  • renderWith() - Render with format (JSON/XML)
  • redirectTo() - Redirect to action/URL
  • flashInsert() - Add flash message
  • sendFile() - Send file download
  • provides() - Set default formats
  • abort() - Stop execution

Parameter Verification Types

  • integer, string, boolean, numeric, date, time, email, url

Flash Message Types

  • success, error, warning, info, notice

Generated by: Wheels Controller Generator Skill v1.0 Framework: CFWheels 3.0+ Last Updated: 2025-10-20