Skip to content

Command Line Tools

Service Architecture Guide

Understanding the Wheels CLI service layer architecture.

The Wheels CLI uses a service-oriented architecture that separates concerns and provides reusable functionality across commands. This architecture makes the CLI more maintainable, testable, and extensible.

┌─────────────────┐
│ Commands │ ← User interacts with
├─────────────────┤
│ Services │ ← Business logic
├─────────────────┤
│ Models │ ← Data and utilities
├─────────────────┤
│ Templates │ ← Code generation
└─────────────────┘

Commands are the user-facing interface:

commands/wheels/generate/model.cfc
component extends="wheels-cli.models.BaseCommand" {
property name="codeGenerationService" inject="CodeGenerationService@wheels-cli";
property name="migrationService" inject="MigrationService@wheels-cli";
function run(
required string name,
boolean migration = true,
string properties = "",
boolean force = false
) {
// Delegate to services
var result = codeGenerationService.generateModel(argumentCollection=arguments);
if (arguments.migration) {
migrationService.createTableMigration(arguments.name, arguments.properties);
}
print.greenLine("✓ Model generated successfully");
}
}

Services contain reusable business logic:

models/CodeGenerationService.cfc
component accessors="true" singleton {
property name="templateService" inject="TemplateService@wheels-cli";
property name="fileService" inject="FileService@wheels-cli";
function generateModel(
required string name,
string properties = "",
struct associations = {},
boolean force = false
) {
var modelName = helpers.capitalize(arguments.name);
var template = templateService.getTemplate("model");
var content = templateService.populateTemplate(template, {
modelName: modelName,
properties: parseProperties(arguments.properties),
associations: arguments.associations
});
return fileService.writeFile(
path = "/models/#modelName#.cfc",
content = content,
force = arguments.force
);
}
}

All commands extend from BaseCommand:

component extends="commandbox.system.BaseCommand" {
property name="print" inject="print";
property name="fileSystemUtil" inject="FileSystem";
function init() {
// Common initialization
return this;
}
// Common helper methods
function ensureDirectoryExists(required string path) {
if (!directoryExists(arguments.path)) {
directoryCreate(arguments.path, true);
}
}
function resolvePath(required string path) {
return fileSystemUtil.resolvePath(arguments.path);
}
}

Manages code generation templates with override system:

  • Template Loading: Searches app snippets first, then CLI templates
  • Variable Substitution: Replaces placeholders with actual values
  • Custom Template Support: Apps can override any CLI template
  • Path Resolution: app/snippets/ overrides /cli/templates/
  • Dynamic Content: Generates form fields, validations, relationships

Key features:

  • Template hierarchy allows project customization
  • Preserves markers for future CLI additions
  • Supports conditional logic in templates
  • Handles both simple and complex placeholders

See Template System Guide for detailed documentation.

Handles database migrations:

  • Generate migration files
  • Track migration status
  • Execute migrations

Testing functionality:

  • Run TestBox tests
  • Generate coverage reports
  • Watch mode support

Centralized code generation:

  • Generate models, controllers, views
  • Handle associations
  • Manage validations

Code analysis tools:

  • Complexity analysis
  • Code style checking
  • Dependency analysis

Security scanning:

  • SQL injection detection
  • XSS vulnerability scanning
  • Hardcoded credential detection

Performance optimization:

  • Cache analysis
  • Query optimization
  • Asset optimization

Plugin management:

  • Install/remove plugins
  • Version management
  • Dependency resolution

Environment management:

  • Environment switching
  • Configuration management
  • Docker/Vagrant support

Services use WireBox for dependency injection:

// In ModuleConfig.cfc
function configure() {
// Service mappings
binder.map("TemplateService@wheels-cli")
.to("wheels.cli.models.TemplateService")
.asSingleton();
binder.map("MigrationService@wheels-cli")
.to("wheels.cli.models.MigrationService")
.asSingleton();
}
models/MyNewService.cfc
component accessors="true" singleton {
// Inject dependencies
property name="fileService" inject="FileService@wheels-cli";
property name="print" inject="print";
function init() {
return this;
}
function doSomething(required string input) {
// Service logic here
return processInput(arguments.input);
}
private function processInput(required string input) {
// Private helper methods
return arguments.input.trim();
}
}

In /ModuleConfig.cfc:

binder.map("MyNewService@wheels-cli")
.to("wheels.cli.models.MyNewService")
.asSingleton();
component extends="wheels-cli.models.BaseCommand" {
property name="myNewService" inject="MyNewService@wheels-cli";
function run(required string input) {
var result = myNewService.doSomething(arguments.input);
print.line(result);
}
}

Most services are singletons:

component singleton {
// Shared instance across commands
}

For creating multiple instances:

component {
function createGenerator(required string type) {
switch(arguments.type) {
case "model":
return new ModelGenerator();
case "controller":
return new ControllerGenerator();
}
}
}

For different implementations:

component {
function setStrategy(required component strategy) {
variables.strategy = arguments.strategy;
}
function execute() {
return variables.strategy.execute();
}
}
component extends="wheels.Testbox" {
function beforeAll() {
// Create service instance
variables.templateService = createMock("wheels.cli.models.TemplateService");
}
function run() {
describe("TemplateService", function() {
it("loads templates correctly", function() {
var template = templateService.getTemplate("model");
expect(template).toBeString();
expect(template).toInclude("component extends=""Model""");
});
it("substitutes variables", function() {
var result = templateService.populateTemplate(
"Hello {{name}}",
{name: "World"}
);
expect(result).toBe("Hello World");
});
});
}
}
function beforeAll() {
// Create mock
mockFileService = createEmptyMock("FileService");
// Define behavior
mockFileService.$("writeFile").$results(true);
// Inject mock
templateService.$property(
propertyName = "fileService",
mock = mockFileService
);
}

Each service should have one clear purpose:

// Good: Focused service
component name="ValidationService" {
function validateModel() {}
function validateController() {}
}
// Bad: Mixed responsibilities
component name="UtilityService" {
function validateModel() {}
function sendEmail() {}
function generatePDF() {}
}

Always inject dependencies:

// Good: Injected dependency
property name="fileService" inject="FileService@wheels-cli";
// Bad: Direct instantiation
variables.fileService = new FileService();

Services should handle errors gracefully:

function generateFile(required string path) {
try {
// Attempt operation
fileWrite(arguments.path, content);
return {success: true};
} catch (any e) {
// Log error
logError(e);
// Return error info
return {
success: false,
error: e.message
};
}
}

Services should be configurable:

component {
property name="settings" inject="coldbox:modulesettings:wheels-cli";
function getTimeout() {
return variables.settings.timeout ?: 30;
}
}

Services can emit events:

// In service
announce("wheels-cli:modelGenerated", {model: modelName});
// In listener
function onModelGenerated(event, data) {
// React to event
}

Services can call each other:

component {
property name="validationService" inject="ValidationService@wheels-cli";
function generateModel() {
// Validate first
if (!validationService.isValidModelName(name)) {
throw("Invalid model name");
}
// Continue generation
}
}
models/plugins/MyServicePlugin.cfc
component implements="IServicePlugin" {
function enhance(required component service) {
// Add new method
arguments.service.myNewMethod = function() {
return "Enhanced!";
};
}
}
component extends="BaseService" {
property name="decoratedService";
function init(required component service) {
variables.decoratedService = arguments.service;
return this;
}
function doSomething() {
// Add behavior
log("Calling doSomething");
// Delegate
return variables.decoratedService.doSomething();
}
}

Load services only when needed:

function getTemplateService() {
if (!structKeyExists(variables, "templateService")) {
variables.templateService = getInstance("TemplateService@wheels-cli");
}
return variables.templateService;
}

Cache expensive operations:

component {
property name="cache" default={};
function getTemplate(required string name) {
if (!structKeyExists(variables.cache, arguments.name)) {
variables.cache[arguments.name] = loadTemplate(arguments.name);
}
return variables.cache[arguments.name];
}
}

Use async for long-running tasks:

function analyzeLargeCodebase() {
thread name="analysis-#createUUID()#" {
// Long running analysis
}
}
component {
property name="log" inject="logbox:logger:{this}";
function doSomething() {
log.debug("Starting doSomething with args: #serializeJSON(arguments)#");
// ... logic ...
log.debug("Completed doSomething");
}
}
Terminal window
# In CommandBox REPL
repl> getInstance("TemplateService@wheels-cli")
repl> getMetadata(getInstance("TemplateService@wheels-cli"))

Planned service architecture improvements:

  1. Service Mesh: Inter-service communication layer
  2. Service Discovery: Dynamic service registration
  3. Circuit Breakers: Fault tolerance patterns
  4. Service Metrics: Performance monitoring
  5. API Gateway: Unified service access