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.php is listed in .gitignore on 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 FormHelper for 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 FlashComponent in 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.
Quick recap:
PracticeCovered in
Lean into naming conventionsMVC Architecture
Keep credentials in app_local.phpDatabase Configuration
Use contain() to avoid N+1 queriesAssociations
Validate at the Table layerValidation
Build forms with FormHelperHelpers
Keep controllers thinThis page
Share logic with ComponentsComponents
You've completed the CakePHP track! Congratulations on working through everything from installation and MVC architecture to the ORM, associations, validation, helpers, and components. The core ideas here — convention over configuration, a thin controller talking to a capable model layer, eager loading associations, and validating data close to the database — aren't unique to CakePHP. They carry straight over into Laravel and Symfony (both still coming to this site), so everything you've practiced here keeps paying off. Click Next below to head back to the PHP hub and pick another track to continue learning.