wheels generate snippets
Generate code snippets and boilerplate code for common patterns.
Synopsis
wheels generate snippets [pattern] [options]
wheels g snippets [pattern] [options]
Description
The wheels generate snippets
command creates code snippets for common Wheels patterns and best practices. It provides ready-to-use code blocks that can be customized for your specific needs, helping you implement standard patterns quickly and consistently.
Arguments
pattern
Snippet pattern to generate
Shows available patterns
Options
--list
List all available snippets
false
--category
Filter by category
All categories
--output
Output format (console, file, clipboard)
console
--customize
Interactive customization
false
--force
Overwrite existing files
false
--help
Show help information
Available Snippets
List All Snippets
wheels generate snippets --list
Output:
Available Snippets:
━━━━━━━━━━━━━━━━━━━
Authentication:
- login-form Login form with remember me
- auth-filter Authentication filter
- password-reset Password reset flow
- user-registration User registration with validation
Model Patterns:
- soft-delete Soft delete implementation
- audit-trail Audit trail with timestamps
- sluggable URL-friendly slugs
- versionable Version tracking
- searchable Full-text search
Controller Patterns:
- crud-actions Complete CRUD actions
- api-controller JSON API controller
- nested-resource Nested resource controller
- admin-controller Admin area controller
View Patterns:
- form-with-errors Form with error handling
- pagination-links Pagination navigation
- search-form Search form with filters
- ajax-form AJAX form submission
Database:
- migration-indexes Common index patterns
- seed-data Database seeding
- constraints Foreign key constraints
Authentication Snippets
Login Form
wheels generate snippets login-form
Generates:
<!--- views/sessions/new.cfm --->
<h1>Login</h1>
#errorMessagesFor("user")#
#startFormTag(action="create", class="login-form")#
<div class="form-group">
#textField(
objectName="user",
property="email",
label="Email",
class="form-control",
placeholder="email@example.com",
required=true,
autofocus=true
)#
</div>
<div class="form-group">
#passwordField(
objectName="user",
property="password",
label="Password",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#checkBox(
objectName="user",
property="rememberMe",
label="Remember me",
value="1"
)#
</div>
<div class="form-group">
#submitTag(value="Login", class="btn btn-primary")#
#linkTo(text="Forgot password?", route="forgotPassword", class="btn btn-link")#
</div>
#endFormTag()#
<!--- controllers/Sessions.cfc --->
component extends="Controller" {
function new() {
user = model("User").new();
}
function create() {
user = model("User").findOne(where="email='#params.user.email#'");
if (IsObject(user) && user.authenticate(params.user.password)) {
session.userId = user.id;
if (params.user.rememberMe == 1) {
cookie.rememberToken = user.generateRememberToken();
cookie.userId = encrypt(user.id, application.encryptionKey);
}
flashInsert(success="Welcome back, #user.firstName#!");
redirectTo(route="dashboard");
} else {
user = model("User").new(email=params.user.email);
flashInsert(error="Invalid email or password.");
renderView(action="new");
}
}
function delete() {
StructDelete(session, "userId");
StructDelete(cookie, "rememberToken");
StructDelete(cookie, "userId");
flashInsert(success="You have been logged out.");
redirectTo(route="home");
}
}
Authentication Filter
wheels generate snippets auth-filter
Generates:
// In Controller.cfc or specific controller
function init() {
filters(through="authenticate", except="new,create");
filters(through="rememberUser", only="new");
}
private function authenticate() {
if (!isLoggedIn()) {
storeLocation();
flashInsert(notice="Please log in to continue.");
redirectTo(route="login");
}
}
private function isLoggedIn() {
return StructKeyExists(session, "userId") && session.userId > 0;
}
private function currentUser() {
if (!StructKeyExists(request, "currentUser")) {
if (isLoggedIn()) {
request.currentUser = model("User").findByKey(session.userId);
} else {
request.currentUser = false;
}
}
return request.currentUser;
}
private function rememberUser() {
if (!isLoggedIn() && StructKeyExists(cookie, "rememberToken")) {
user = model("User").findOne(
where="rememberToken='#cookie.rememberToken#' AND rememberTokenExpiresAt > NOW()"
);
if (IsObject(user)) {
session.userId = user.id;
user.updateRememberToken();
}
}
}
private function storeLocation() {
if (request.method == "GET") {
session.returnTo = request.cgi.path_info;
if (Len(request.cgi.query_string)) {
session.returnTo &= "?" & request.cgi.query_string;
}
}
}
private function redirectBackOrTo(required string route) {
if (StructKeyExists(session, "returnTo")) {
local.returnTo = session.returnTo;
StructDelete(session, "returnTo");
redirectTo(location=local.returnTo);
} else {
redirectTo(route=arguments.route);
}
}
Model Patterns
Soft Delete
wheels generate snippets soft-delete
Generates:
// In Model init()
property(name="deletedAt", sql="deleted_at");
// Soft delete callbacks
beforeDelete("softDelete");
afterFind("excludeDeleted");
// Default scope
function excludeDeleted() {
if (!StructKeyExists(arguments, "includeSoftDeleted") || !arguments.includeSoftDeleted) {
if (StructKeyExists(this, "deletedAt") && !IsNull(this.deletedAt)) {
return false; // Exclude from results
}
}
}
// Soft delete implementation
private function softDelete() {
this.deletedAt = Now();
this.save(validate=false, callbacks=false);
return false; // Prevent actual deletion
}
// Scopes
function active() {
return this.findAll(where="deleted_at IS NULL", argumentCollection=arguments);
}
function deleted() {
return this.findAll(where="deleted_at IS NOT NULL", argumentCollection=arguments);
}
function withDeleted() {
return this.findAll(includeSoftDeleted=true, argumentCollection=arguments);
}
// Restore method
function restore() {
this.deletedAt = "";
return this.save(validate=false);
}
// Permanent delete
function forceDelete() {
return this.delete(callbacks=false);
}
Audit Trail
wheels generate snippets audit-trail --customize
Interactive customization:
? Include user tracking? (Y/n) › Y
? Track IP address? (y/N) › Y
? Track changes in JSON? (Y/n) › Y
Generates:
// models/AuditLog.cfc
component extends="Model" {
function init() {
belongsTo("user");
property(name="modelName", sql="model_name");
property(name="recordId", sql="record_id");
property(name="action", sql="action");
property(name="changes", sql="changes");
property(name="userId", sql="user_id");
property(name="ipAddress", sql="ip_address");
property(name="userAgent", sql="user_agent");
validatesPresenceOf("modelName,recordId,action");
}
}
// In audited model
function init() {
afterCreate("logCreate");
afterUpdate("logUpdate");
afterDelete("logDelete");
}
private function logCreate() {
createAuditLog("create", this.properties());
}
private function logUpdate() {
if (hasChanged()) {
createAuditLog("update", this.changedProperties());
}
}
private function logDelete() {
createAuditLog("delete", {id: this.id});
}
private function createAuditLog(required string action, required struct data) {
model("AuditLog").create({
modelName: ListLast(GetMetaData(this).name, "."),
recordId: this.id,
action: arguments.action,
changes: SerializeJSON(arguments.data),
userId: request.currentUser.id ?: "",
ipAddress: request.remoteAddress,
userAgent: request.userAgent
});
}
private function changedProperties() {
local.changes = {};
local.properties = this.properties();
for (local.key in local.properties) {
if (hasChanged(local.key)) {
local.changes[local.key] = {
from: this.changedFrom(local.key),
to: local.properties[local.key]
};
}
}
return local.changes;
}
// Audit log migration
component extends="wheels.migrator.Migration" {
function up() {
createTable(name="audit_logs") {
t.increments("id");
t.string("model_name", null=false);
t.integer("record_id", null=false);
t.string("action", null=false);
t.text("changes");
t.integer("user_id");
t.string("ip_address");
t.string("user_agent");
t.timestamps();
t.index(["model_name", "record_id"]);
t.index("user_id");
t.index("created_at");
};
}
function down() {
dropTable("audit_logs");
}
}
Controller Patterns
CRUD Actions
wheels generate snippets crud-actions
Generates complete CRUD controller with error handling, pagination, and filters.
API Controller
wheels generate snippets api-controller
Generates:
component extends="Controller" {
function init() {
provides("json");
filters(through="setApiHeaders");
filters(through="authenticateApi");
filters(through="logApiRequest", except="index,show");
}
private function setApiHeaders() {
header name="X-API-Version" value="1.0";
header name="X-RateLimit-Limit" value="1000";
header name="X-RateLimit-Remaining" value=getRateLimitRemaining();
}
private function authenticateApi() {
local.token = getAuthToken();
if (!Len(local.token)) {
renderUnauthorized("Missing authentication token");
}
request.apiUser = model("ApiKey").authenticate(local.token);
if (!IsObject(request.apiUser)) {
renderUnauthorized("Invalid authentication token");
}
}
private function getAuthToken() {
// Check Authorization header
if (StructKeyExists(getHttpRequestData().headers, "Authorization")) {
local.auth = getHttpRequestData().headers.Authorization;
if (Left(local.auth, 7) == "Bearer ") {
return Mid(local.auth, 8, Len(local.auth));
}
}
// Check X-API-Key header
if (StructKeyExists(getHttpRequestData().headers, "X-API-Key")) {
return getHttpRequestData().headers["X-API-Key"];
}
// Check query parameter
if (StructKeyExists(params, "api_key")) {
return params.api_key;
}
return "";
}
private function renderUnauthorized(required string message) {
renderWith(
data={
error: {
code: 401,
message: arguments.message
}
},
status=401
);
}
private function renderError(required string message, numeric status = 400) {
renderWith(
data={
error: {
code: arguments.status,
message: arguments.message
}
},
status=arguments.status
);
}
private function renderSuccess(required any data, numeric status = 200) {
renderWith(
data={
success: true,
data: arguments.data
},
status=arguments.status
);
}
private function renderPaginated(required any query) {
renderWith(
data={
success: true,
data: arguments.query,
pagination: {
page: arguments.query.currentPage,
perPage: arguments.query.perPage,
total: arguments.query.totalRecords,
pages: arguments.query.totalPages
}
}
);
}
}
View Patterns
Form with Errors
wheels generate snippets form-with-errors
AJAX Form
wheels generate snippets ajax-form
Generates:
<!--- View file --->
<div id="contact-form-container">
#startFormTag(
action="send",
id="contact-form",
class="ajax-form",
data={
remote: true,
method: "post",
success: "handleFormSuccess",
error: "handleFormError"
}
)#
<div class="form-messages" style="display: none;"></div>
<div class="form-group">
#textField(
name="name",
label="Name",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#emailField(
name="email",
label="Email",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#textArea(
name="message",
label="Message",
class="form-control",
rows=5,
required=true
)#
</div>
<div class="form-group">
#submitTag(
value="Send Message",
class="btn btn-primary",
data={loading: "Sending..."}
)#
</div>
#endFormTag()#
</div>
<script>
// AJAX form handler
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('contact-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const submitBtn = form.querySelector('[type="submit"]');
const originalText = submitBtn.value;
const loadingText = submitBtn.dataset.loading;
// Disable form
submitBtn.disabled = true;
submitBtn.value = loadingText;
// Send AJAX request
fetch(form.action, {
method: form.method,
body: new FormData(form),
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
handleFormSuccess(data);
} else {
handleFormError(data);
}
})
.catch(error => {
handleFormError({message: 'Network error. Please try again.'});
})
.finally(() => {
submitBtn.disabled = false;
submitBtn.value = originalText;
});
});
});
function handleFormSuccess(data) {
const form = document.getElementById('contact-form');
const messages = form.querySelector('.form-messages');
// Show success message
messages.className = 'form-messages alert alert-success';
messages.textContent = data.message || 'Message sent successfully!';
messages.style.display = 'block';
// Reset form
form.reset();
// Hide message after 5 seconds
setTimeout(() => {
messages.style.display = 'none';
}, 5000);
}
function handleFormError(data) {
const form = document.getElementById('contact-form');
const messages = form.querySelector('.form-messages');
// Show error message
messages.className = 'form-messages alert alert-danger';
messages.textContent = data.message || 'An error occurred. Please try again.';
messages.style.display = 'block';
// Show field errors
if (data.errors) {
Object.keys(data.errors).forEach(field => {
const input = form.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
const error = document.createElement('div');
error.className = 'invalid-feedback';
error.textContent = data.errors[field].join(', ');
input.parentNode.appendChild(error);
}
});
}
}
</script>
<!--- Controller action --->
function send() {
contact = model("Contact").new(params);
if (contact.save()) {
if (isAjax()) {
renderWith(data={
success: true,
message: "Thank you! We'll be in touch soon."
});
} else {
flashInsert(success="Thank you! We'll be in touch soon.");
redirectTo(route="home");
}
} else {
if (isAjax()) {
renderWith(data={
success: false,
message: "Please correct the errors below.",
errors: contact.allErrors()
}, status=422);
} else {
renderView(action="new");
}
}
}
Database Snippets
Migration Indexes
wheels generate snippets migration-indexes
Generates common index patterns:
// Performance indexes
t.index("email"); // Single column
t.index(["last_name", "first_name"]); // Composite
t.index("created_at"); // Timestamp queries
// Unique constraints
t.index("email", unique=true);
t.index(["user_id", "role_id"], unique=true);
// Foreign key indexes
t.index("user_id");
t.index("category_id");
// Full-text search
t.index("title", type="fulltext");
t.index(["title", "content"], type="fulltext");
// Partial indexes (PostgreSQL)
t.index("email", where="deleted_at IS NULL");
// Expression indexes
t.index("LOWER(email)", name="idx_email_lower");
Seed Data
wheels generate snippets seed-data
Custom Snippets
Create Custom Snippet
wheels generate snippets --create=my-pattern
Creates template in ~/.wheels/snippets/my-pattern/
:
my-pattern/
├── snippet.json
├── files/
│ ├── controller.cfc
│ ├── model.cfc
│ └── view.cfm
└── README.md
snippet.json
:
{
"name": "my-pattern",
"description": "Custom pattern description",
"category": "custom",
"author": "Your Name",
"version": "1.0.0",
"variables": [
{
"name": "modelName",
"prompt": "Model name?",
"default": "MyModel"
}
],
"files": [
{
"source": "files/controller.cfc",
"destination": "controllers/${controllerName}.cfc"
}
]
}
Output Options
Copy to Clipboard
wheels generate snippets login-form --output=clipboard
Save to File
wheels generate snippets api-controller --output=file --path=./controllers/Api.cfc
Interactive Mode
wheels generate snippets --customize
Best Practices
Review generated code: Customize for your needs
Understand the patterns: Don't blindly copy
Keep snippets updated: Maintain with framework updates
Share useful patterns: Contribute back to community
Document customizations: Note changes made
Test generated code: Ensure it works in your context
Use consistent patterns: Across your application
See Also
Last updated
Was this helpful?