Claude Code Plugins

Community-maintained marketplace

Feedback

Wheels Controller Generator

@wheels-dev/wheels
202
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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