Core Concepts
Conventions over Configuration
Wheels trades explicit configuration for naming conventions. A file in the right place with the right name wires itself into the framework without a line of setup — and when a convention doesn’t fit your schema, you override it in one line and move on.
You’ll learn:
- What “convention over configuration” means in practice
- Which conventions Wheels enforces and what each one infers
- What breaking a convention costs
- When an override is the right call
What the convention buys you
Section titled “What the convention buys you”Naming is the glue between layers. The framework reads the name of a file, class, action, or association and fills in the rest — table name, view path, routes, foreign keys — without any registration step. You get a working surface area from the filename alone.
| You create | Wheels infers |
|---|---|
app/models/Post.cfc | Model class Post, table posts, primary key id, datasource = app default |
app/controllers/Posts.cfc | Controller Posts, view dir app/views/posts/, routes via .resources("posts") |
Action index on Posts | View path app/views/posts/index.cfm, rendered through app/views/layout.cfm |
config/routes.cfm with .resources("posts") | Seven routes (posts/newPost/post/editPost named), CRUD mapping |
hasMany(name="comments") on Post | Foreign key postId on comments; methods post.comments() and post.createComment() |
Every row in that table is a decision you didn’t have to make. No registry file lists your models. No manifest maps actions to views. No config block wires a controller to a route. Rename a file and the wiring follows. Delete a file and the wiring is gone.
The rules themselves are narrow and mechanical. Model classes are singular PascalCase; table names are the plural, lowercase form of the class name. Controller classes are plural PascalCase; the view directory is the lowercase form of the controller name. Primary keys are id. Foreign keys are the singular association name plus Id — postId for a belongsTo("post"), userId for a belongsTo("user"). Every rule follows the same shape: given a name, there is exactly one location the framework will look, and exactly one shape it expects.
How to override when you need to
Section titled “How to override when you need to”Every convention has a one-line escape hatch. The overrides live where the convention would have applied — in the model’s config(), in the route definition, in the renderView() call. You don’t reach into framework internals to change the mapping; you declare the exception next to the thing it applies to.
- Custom table:
tableName("old_blog_entries")inside the model’sconfig(). - Custom primary key:
setPrimaryKey("entryId")insideconfig(). Composite keys pass an array. - Custom datasource:
dataSource("legacy")insideconfig()picks a non-default datasource for this model. - Custom foreign key on an association:
hasMany(name="comments", foreignKey="blogPostId")overrides thepostIddefault. - Custom view path:
renderView(action="summary")orrenderView(controller="shared", action="error")picks a different template without renaming the action. - Custom layout:
renderView(layout="print")for one action, or afilters(through="$usePrintLayout")filter that callsusesLayout("print")across a controller. - Custom route mapping:
.get(pattern="/old/:key", to="posts##show")inconfig/routes.cfmmaps a legacy URL onto a conventional action.
Each override is local. The rest of the model, the rest of the controller, the rest of the app keeps the default behavior. You pay the configuration cost only where the convention didn’t fit.
The same shape holds for every override in the framework. The convention fires by default; a single named argument or a single line in config() replaces it with a value you supply. You don’t disable conventions globally, and there’s no flag that turns them off. The surface area stays predictable because every exception is visible next to the thing that needed it.
When conventions fight you
Section titled “When conventions fight you”Legacy databases are the usual source of friction. Tables named tbl_user_data, primary keys called userID instead of id, pluralizations Wheels’ inflector doesn’t recognize (person/people, datum/data, goose/geese), columns that carry JSON or enum values on a schema designed before either was a standard type. Multi-tenancy with per-tenant schemas or table prefixes. Foreign keys that don’t follow the <model>Id pattern because someone named them after a product code twenty years ago.
None of these block you. Each has a declared override — tableName(), setPrimaryKey(), foreignKey=, a custom inflector rule, a dynamic scope, a middleware that resolves the tenant. The cost is a handful of extra lines in the model and a reader who has to know the schema is unusual. That’s a real cost, but it’s paid once per table, not once per query. Most apps have five or ten such exceptions and several hundred conventional models.
The pattern to avoid is configuring away a convention you could have followed. If a new table is going in and the name is negotiable, pick the plural lowercase form the framework expects. If a new foreign key is going in, name it <model>Id. The conventions cost nothing when you adopt them and compound every time you add a model, an association, or a controller. The exceptions are for the schemas you inherited, not the schemas you write.
Why this philosophy
Section titled “Why this philosophy”Conventions reduce the decision surface. A developer opening a Wheels app for the first time already knows where models, controllers, views, migrations, and routes live — the directory structure is the same in every Wheels app. They know that Post.cfc maps to a posts table without reading your configuration. They know the show action renders posts/show.cfm. The part of the codebase they have to read to understand what’s happening is the part you actually wrote, not the scaffolding around it.
Tooling depends on the same assumptions. Generators produce files at the expected paths. Scaffolds wire controllers to views to routes because the routes and views are where the conventions say they are. The wheels CLI, migrations, seeds, and test runners all rely on the convention to locate the things they operate on. Breaking a convention shifts cost from the framework to your app: the tools stop helping, and every new developer has to learn one more thing that isn’t in the docs. Override where you must, follow the convention where you can.
See also
Section titled “See also”- MVC in Wheels — the layer boundaries conventions glue together
- How Routing Works — the resource-to-routes convention
- ORM Philosophy — ActiveRecord-style conventions
- Models and the ORM — hands-on overrides