Best Practices
This page is a capstone checklist for the whole CakePHP track — the habits worth carrying into every real project you build with it. Each item links back to the page that covers it in full, so treat this as a quick-reference summary rather than a first read.
1) Lean Into Conventions, Don't Fight Them
CakePHP's entire productivity pitch rests on convention over configuration. Name your
table articles, your Table class ArticlesTable, and your controller
ArticlesController, and the framework wires them together with no configuration at all.
Renaming things to suit personal taste, then configuring CakePHP to understand the new names, almost
always means more code, not less. See MVC Architecture.
articles table -> ArticlesTable -> ArticlesController
- Follow CakePHP's naming conventions by default; only override them with a strong reason.
- Before writing custom configuration to rename something, ask whether renaming is really worth it.
2) Keep Real Credentials Out of Version Control
Database usernames and passwords belong in config/app_local.php, which is git-ignored by
default, never in config/app.php, which is committed. Anyone who clones the repository gets
working defaults from app.php and supplies their own real credentials locally. See
Database Configuration.
<?php
// config/app_local.php — real credentials, never committed
return [
'Datasources' => [
'default' => [
'username' => 'real_user',
'password' => 'real_password',
],
],
];
- Confirm
app_local.phpis listed in.gitignoreon every project. - Never paste a real password directly into
app.php, even temporarily.
3) Use contain() to Avoid N+1 Queries
Looping over a result set and querying an association inside the loop fires one extra query per row
— the classic N+1 problem. contain() tells the ORM to eager-load the association in one
extra query up front, regardless of how many rows come back. See
Associations.
$articles = $this->Articles->find()
->contain(['Comments'])
->all();
- Reach for
contain()any time a view needs an associated model's data. - Watch your query log during development for repeated queries inside a loop — a sign
contain()is missing.
4) Always Validate at the Table Layer
Client-side validation is a UX convenience, not a security boundary — anyone can bypass JavaScript
and submit whatever they want directly to your server. Rules in validationDefault() run on
every save regardless of what the browser did or didn't check, so that's where real protection lives. See
Validation.
public function validationDefault(Validator $validator): Validator
{
$validator->requirePresence('title')->notEmptyString('title');
return $validator;
}
- Treat any client-side validation as a nicety for the user, never as your only line of defense.
- Add or update
validationDefault()rules whenever a field's requirements change.
5) Build Forms With FormHelper, Not Raw HTML
Hand-written <form> markup means hand-written CSRF tokens and hand-written error
display — both easy to forget. FormHelper gives you both for free: Form->create()
includes a CSRF token automatically, and Form->control() shows a field's validation error
right beneath it whenever one exists. See Helpers.
<?= $this->Form->create($article) ?>
<?= $this->Form->control('title') ?>
<?= $this->Form->end() ?>
- Default to
FormHelperfor every form tied to a model. - Only drop to raw HTML for forms with nothing to do with an entity, and add CSRF handling yourself if needed.
6) Keep Controllers Thin
A controller action's job is to receive the request, ask the model layer to do something, and hand a result to the view — not to contain business logic itself. Push data rules, calculations, and query logic into Table and Entity methods, where they're easier to test and easier to reuse from more than one action.
// Thin: the controller just delegates
public function index()
{
$this->set('articles', $this->Articles->findPublished());
}
- If an action is doing calculations or complex conditionals, consider moving that logic to the Table or Entity.
- A controller action that's mostly delegation is usually a sign you're on the right track.
7) Share Controller Logic With Components
When more than one controller needs the same controller-layer logic — flash messages, an authentication check, a rate limiter — write or load a component instead of copy-pasting the same code into every controller's actions. See Components.
$this->loadComponent('Flash');
- Load
FlashComponentin any controller that needs to message the user after an action. - Write a custom component once logic is needed by two or more controllers, instead of duplicating it.
| Practice | Covered in |
|---|---|
| Lean into naming conventions | MVC Architecture |
Keep credentials in app_local.php | Database Configuration |
Use contain() to avoid N+1 queries | Associations |
| Validate at the Table layer | Validation |
| Build forms with FormHelper | Helpers |
| Keep controllers thin | This page |
| Share logic with Components | Components |