Loading...

CLI Overview

Quick Start Guide

wheels info

wheels reload

wheels deps

wheels destroy

wheels watch

wheels generate app

wheels generate app-wizard

wheels generate controller

wheels generate model

wheels generate view

wheels generate property

wheels generate route

wheels generate resource

wheels generate api-resource

wheels generate frontend

wheels generate test

wheels generate snippets

wheels scaffold

wheels test

wheels test run

wheels test coverage

wheels test debug

wheels config list

wheels config set

wheels config env

wheels env

wheels env setup

wheels env list

wheels env switch

wheels environment

wheels console

wheels runner

wheels server

wheels server start

wheels server stop

wheels server restart

wheels server status

wheels server log

wheels server open

wheels plugins

wheels plugins list

wheels plugins install

wheels plugins remove

wheels analyze

wheels analyze code

wheels analyze performance

wheels analyze security

wheels security

wheels security scan

wheels optimize

wheels optimize performance

wheels docs

wheels docs generate

wheels docs serve

wheels ci init

wheels docker init

wheels docker deploy

wheels deploy

wheels deploy audit

wheels deploy exec

wheels deploy hooks

wheels deploy init

wheels deploy lock

wheels deploy logs

wheels deploy proxy

wheels deploy push

wheels deploy rollback

wheels deploy secrets

wheels deploy setup

wheels deploy status

wheels deploy stop

Configuration Management

Creating Commands

Service Architecture

Migrations Guide

Testing Guide

Ask or search...
Ctrl K
Loading...

wheels generate resource (Coming Soon)

This command may not work as expected. A complete and stable version is coming soon.

Generate a complete RESTful resource with model, controller, views, and routes.

Synopsis

wheels generate resource [name] [properties] [options]
wheels g resource [name] [properties] [options]

Description

The wheels generate resource command creates a complete RESTful resource including model, controller with all CRUD actions, views, routes, and optionally database migrations and tests. It's a comprehensive generator that sets up everything needed for a functioning resource.

Arguments

| Argument | Description | Default | |----------|-------------|---------| | name | Resource name (singular) | Required |

Options

| Option | Description | Default | |--------|-------------|---------| | --api | Generate API-only resource (no views) | false | | --tests | Generate associated tests | true | | --migration | Generate database migration | true | | belongs-to | Parent model relationships (comma-separated) | | | has-many | Child model relationships (comma-separated) | | | attributes | Model attributes (name:type,email:string) | | | --open | Open generated files | false | | --scaffold | Generate with full CRUD operations | true |

Examples

Basic Resource

wheels generate resource product attributes="name:string,price:float,description:text"

Generates:

  • Model: /models/Product.cfc
  • Controller: /controllers/Products.cfc
  • Views: /views/products/ (index, show, new, edit, _form)
  • Route: resources("products") in /config/routes.cfm
  • Migration: /app/migrator/migrations/[timestamp]_create_products.cfc
  • Tests: /tests/models/ProductTest.cfc, /tests/controllers/ProductsTest.cfc

API Resource

wheels generate resource product attributes="name:string,price:float" --api

Generates:

  • Model: /models/Product.cfc
  • Controller: /controllers/api/Products.cfc (JSON responses only)
  • Route: resources(name="products", except="new,edit") in API namespace
  • Migration: /app/migrator/migrations/[timestamp]_create_products.cfc
  • Tests: API-focused test files

Resource with Associations

wheels generate resource comment attributes="content:text,approved:boolean" belongs-to="post,user"

Generates nested structure with proper associations and routing.

Generated Files

Model

/models/Product.cfc:

component extends="Model" {
    
    function init() {
        // Properties
        property(name="name", sql="name");
        property(name="price", sql="price");
        property(name="description", sql="description");
        
        // Validations
        validatesPresenceOf(properties="name,price");
        validatesNumericalityOf(property="price", greaterThan=0);
        validatesLengthOf(property="name", maximum=255);
        
        // Callbacks
        beforeSave("sanitizeInput");
    }
    
    private function sanitizeInput() {
        this.name = Trim(this.name);
        if (StructKeyExists(this, "description")) {
            this.description = Trim(this.description);
        }
    }
    
}

Controller

/controllers/Products.cfc:

component extends="Controller" {
    
    function init() {
        // Filters
        filters(through="findProduct", only="show,edit,update,delete");
    }
    
    function index() {
        products = model("Product").findAll(order="createdAt DESC");
    }
    
    function show() {
        // Product loaded by filter
    }
    
    function new() {
        product = model("Product").new();
    }
    
    function create() {
        product = model("Product").new(params.product);
        
        if (product.save()) {
            flashInsert(success="Product was created successfully.");
            redirectTo(route="product", key=product.id);
        } else {
            renderView(action="new");
        }
    }
    
    function edit() {
        // Product loaded by filter
    }
    
    function update() {
        if (product.update(params.product)) {
            flashInsert(success="Product was updated successfully.");
            redirectTo(route="product", key=product.id);
        } else {
            renderView(action="edit");
        }
    }
    
    function delete() {
        if (product.delete()) {
            flashInsert(success="Product was deleted successfully.");
        } else {
            flashInsert(error="Product could not be deleted.");
        }
        redirectTo(route="products");
    }
    
    // Filters
    private function findProduct() {
        product = model("Product").findByKey(params.key);
        if (!IsObject(product)) {
            flashInsert(error="Product not found.");
            redirectTo(route="products");
        }
    }
    
}

Views

/views/products/index.cfm:

<h1>Products</h1>

<p>
    #linkTo(route="newProduct", text="New Product", class="btn btn-primary")#
</p>

<cfif products.recordCount>
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Price</th>
                <th>Created</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            <cfoutput query="products">
                <tr>
                    <td>#linkTo(route="product", key=products.id, text=products.name)#</td>
                    <td>#dollarFormat(products.price)#</td>
                    <td>#dateFormat(products.createdAt, "mmm dd, yyyy")#</td>
                    <td>
                        #linkTo(route="editProduct", key=products.id, text="Edit", class="btn btn-sm btn-secondary")#
                        #linkTo(route="product", key=products.id, text="Delete", method="delete", confirm="Are you sure?", class="btn btn-sm btn-danger")#
                    </td>
                </tr>
            </cfoutput>
        </tbody>
    </table>
<cfelse>
    <p class="alert alert-info">No products found. #linkTo(route="newProduct", text="Create one now")#!</p>
</cfif>

/views/products/_form.cfm:

#errorMessagesFor("product")#

#startFormTag(route=formRoute, key=formKey, class="needs-validation")#
    
    <div class="mb-3">
        #textField(objectName="product", property="name", label="Product Name", class="form-control", required=true)#
    </div>
    
    <div class="mb-3">
        #numberField(objectName="product", property="price", label="Price", class="form-control", step="0.01", min="0", required=true)#
    </div>
    
    <div class="mb-3">
        #textArea(objectName="product", property="description", label="Description", class="form-control", rows=5)#
    </div>
    
    <div class="mb-3">
        #submitTag(value=submitValue, class="btn btn-primary")#
        #linkTo(route="products", text="Cancel", class="btn btn-secondary")#
    </div>
    
#endFormTag()#

Migration

/app/migrator/migrations/[timestamp]_create_products.cfc:

component extends="wheels.migrator.Migration" hint="Create products table" {
    
    function up() {
        transaction {
            createTable(name="products", force=true) {
                t.increments("id");
                t.string("name", limit=255, null=false);
                t.decimal("price", precision=10, scale=2, null=false);
                t.text("description");
                t.timestamps();
                t.index("name");
            };
        }
    }
    
    function down() {
        transaction {
            dropTable("products");
        }
    }
    
}

Routes

Added to /config/routes.cfm:

<cfset resources("products")>

API Resource Generation

API Controller

/controllers/api/Products.cfc:

component extends="Controller" {
    
    function init() {
        provides("json");
        filters(through="findProduct", only="show,update,delete");
    }
    
    function index() {
        products = model("Product").findAll(
            order="createdAt DESC",
            page=params.page ?: 1,
            perPage=params.perPage ?: 25
        );
        
        renderWith({
            data: products,
            meta: {
                page: products.currentPage,
                totalPages: products.totalPages,
                totalRecords: products.totalRecords
            }
        });
    }
    
    function show() {
        renderWith(product);
    }
    
    function create() {
        product = model("Product").new(params.product);
        
        if (product.save()) {
            renderWith(data=product, status=201);
        } else {
            renderWith(
                data={errors: product.allErrors()},
                status=422
            );
        }
    }
    
    function update() {
        if (product.update(params.product)) {
            renderWith(product);
        } else {
            renderWith(
                data={errors: product.allErrors()},
                status=422
            );
        }
    }
    
    function delete() {
        if (product.delete()) {
            renderWith(data={message: "Product deleted successfully"});
        } else {
            renderWith(
                data={error: "Could not delete product"},
                status=400
            );
        }
    }
    
    private function findProduct() {
        product = model("Product").findByKey(params.key);
        if (!IsObject(product)) {
            renderWith(
                data={error: "Product not found"},
                status=404
            );
        }
    }
    
}

Nested Resources

Generate Nested Resource

wheels generate resource review rating:integer comment:text parent=product

Nested Model

Includes association:

component extends="Model" {
    
    function init() {
        belongsTo("product");
        
        property(name="rating", sql="rating");
        property(name="comment", sql="comment");
        property(name="productId", sql="product_id");
        
        validatesPresenceOf(properties="rating,comment,productId");
        validatesNumericalityOf(property="rating", greaterThanOrEqualTo=1, lessThanOrEqualTo=5);
    }
    
}

Nested Routes

<cfset resources("products")>
    <cfset resources("reviews")>
</cfset>

Property Types

Supported Types

| Type | Database Type | Validation | |------|--------------|------------| | string | VARCHAR(255) | Length validation | | text | TEXT | None by default | | integer | INT | Numerical validation | | float | DECIMAL | Numerical validation | | decimal | DECIMAL | Numerical validation | | boolean | BOOLEAN | Boolean validation | | date | DATE | Date format validation | | datetime | DATETIME | DateTime validation | | time | TIME | Time validation | | binary | BLOB | None |

Property Options

wheels generate resource user \
  email:string:required:unique \
  age:integer:min=18:max=120 \
  bio:text:limit=1000 \
  isActive:boolean:default=true

Advanced Options

Skip Components

# Generate only model and migration
wheels generate resource product name:string --skip-controller --skip-views --skip-route

# Generate only controller and views
wheels generate resource product --skip-model --skip-migration

Namespace Resources

wheels generate resource admin/product name:string namespace=admin

Creates:

  • /controllers/admin/Products.cfc
  • /views/admin/products/
  • Namespaced routes

Custom Templates

wheels generate resource product name:string template=custom

Testing

Generated Tests

Model Test (/tests/models/ProductTest.cfc):

component extends="wheels.Test" {
    
    function setup() {
        super.setup();
        model("Product").deleteAll();
    }
    
    function test_valid_product_saves() {
        product = model("Product").new(
            name="Test Product",
            price=19.99,
            description="Test description"
        );
        
        assert(product.save());
        assert(product.id > 0);
    }
    
    function test_requires_name() {
        product = model("Product").new(price=19.99);
        
        assert(!product.save());
        assert(ArrayLen(product.errorsOn("name")) > 0);
    }
    
    function test_requires_positive_price() {
        product = model("Product").new(name="Test", price=-10);
        
        assert(!product.save());
        assert(ArrayLen(product.errorsOn("price")) > 0);
    }
    
}

Controller Test (/tests/controllers/ProductsTest.cfc):

component extends="wheels.Test" {
    
    function test_index_returns_products() {
        products = createProducts(3);
        
        result = processRequest(route="products", method="GET");
        
        assert(result.status == 200);
        assert(Find("Products", result.body));
        assert(FindNoCase(products[1].name, result.body));
    }
    
    function test_create_valid_product() {
        params = {
            product: {
                name: "New Product",
                price: 29.99,
                description: "New product description"
            }
        };
        
        result = processRequest(route="products", method="POST", params=params);
        
        assert(result.status == 302);
        assert(model("Product").count() == 1);
    }
    
}

Best Practices

  1. Use singular names: product not products
  2. Define all properties: Include types and validations
  3. Add indexes: For frequently queried fields
  4. Include tests: Don't skip test generation
  5. Use namespaces: For admin or API resources
  6. Follow conventions: Stick to RESTful patterns

Common Patterns

Soft Delete Resource

wheels generate resource product name:string deletedAt:datetime:nullable

Publishable Resource

wheels generate resource post title:string content:text publishedAt:datetime:nullable status:string:default=draft

User-Owned Resource

wheels generate resource task title:string userId:integer:belongsTo=user completed:boolean:default=false

Hierarchical Resource

wheels generate resource category name:string parentId:integer:nullable:belongsTo=category

Customization

Custom Resource Templates

Create in ~/.wheels/templates/resources/:

custom-resource/
├── model.cfc
├── controller.cfc
├── views/
│   ├── index.cfm
│   ├── show.cfm
│   ├── new.cfm
│   ├── edit.cfm
│   └── _form.cfm
└── migration.cfc

Template Variables

Available in templates:

  • ${resourceName} - Singular name
  • ${resourceNamePlural} - Plural name
  • ${modelName} - Model class name
  • ${controllerName} - Controller class name
  • ${tableName} - Database table name
  • ${properties} - Array of property definitions

See Also