| 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 viewrenderPartial()- Render partialrenderWith()- Render with format (JSON/XML)redirectTo()- Redirect to action/URLflashInsert()- Add flash messagesendFile()- Send file downloadprovides()- Set default formatsabort()- 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