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.
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");
}
}
}
// ❌ 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);
}
}
}
function create() {
resource = model("Resource").new(params.resource);
if (!resource.save()) {
// Render the new action's view
renderView(action="new");
}
}
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();