This article is part of the CakeDC Advent Calendar 2025 (December 2th 2025)
CounterCacheBehavior in CakePHP: what it is, when to use it, and what’s new in CakePHP 5.2
As your application grows, a very common pattern appears: you need to display things like “number of comments”, “number of tasks”, or “number of orders”, and you need to do it fast. Calculating these values with COUNT() queries can work until performance starts to suffer (and complexity increases because of filters, states, or joins). This is exactly where CounterCacheBehavior* becomes useful.
What is CounterCacheBehavior?
CounterCacheBehavior is a CakePHP ORM behavior that keeps a counter field in a “parent” table synchronized based on the records in a related table. Typical example:
- Articles hasMany Comments
- You want to store the number of comments in articles.comment_count
The behavior automatically increments, decrements, or recalculates that value when related records are created, deleted, or modified.
When should you use it?
Common use cases include:
- Listings with counters (e.g. “Posts (123 comments)”).
- Sorting by counters (most commented, most active, etc.).
- Filtering by counters (categories with more than X products).
- Avoiding repeated and expensive COUNT( ) queries.
The idea is simple: accept a small cost on writes in exchange for much faster reads.
Basic configuration
CounterCache is configured in the child table (the one that belongs to the parent). If Comments belongsTo Articles, the behavior lives in CommentsTable.
// src/Model/Table/CommentsTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
class CommentsTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->belongsTo('Articles');
$this->addBehavior('CounterCache', [
'Articles' => ['comment_count']
]);
}
}
Doing this, CakePHP will automatically keep articles.comment_count up to date.
CounterCache with conditions (scoped counters)
Often you don’t want to count everything, but only a subset: published comments, active records, non-spam items, etc.
$this->addBehavior('CounterCache', [
'Articles' => [
'published_comment_count' => [
'conditions' => ['Comments.is_published' => true]
]
]
]);
This pattern is very useful for dashboards such as:
- open issues.
- completed tasks.
- approved records.
CounterCache with callbacks (custom calculations)
In some cases, conditions are not enough and you need more complex logic (joins, dynamic filters, or advanced queries). CounterCacheBehavior allows you to define a callable to calculate the counter value.
Important: when using callbacks, bulk updates with updateCounterCache() will not update counters defined with closures. This is an important limitation to keep in mind.
What’s new in CakePHP 5.2: rebuild counters from the console
Before CakePHP 5.2, rebuilding counters often meant writing your own scripts or commands, especially after:
- bulk imports done directly in the database.
- manual data fixes.
- adding a new counter cache in production.
- data becoming out of sync.
New command: bashbin/cake counter_cache
CakePHP 5.2 introduced an official command to rebuild counter caches:
bin/cake counter_cache --assoc Comments Articles
This command recalculates all counters related to Comments in the Articles table.
Processing large tables in batches
For large datasets, you can rebuild counters in chunks:
bin/cake counter_cache --assoc Comments --limit 100 --page 2 Articles
When using --limit and --page, records are processed ordered by the table’s primary key. This command is ideal for maintenance tasks and for safely backfilling new counter caches without custom tooling.
What’s new in CakePHP 5.2: bulk updates from the ORM
In addition to the console command, CakePHP 5.2 added a new ORM method:
CounterCacheBehavior::updateCounterCache()
This allows you to update counters programmatically, in batches:
// Update all configured counter caches in batches
$this->Comments->updateCounterCache();
// Update only a specific association, 200 records per batch
$this->Comments->updateCounterCache('Articles', 200);
// Update only the first page
$this->Comments->updateCounterCache('Articles', page: 1);
This is available since CakePHP 5.2.0.
Complete practical example: Articles and Comments
Assume the following database structure:
- articles: id, title, comment_count (int, default 0), published_comment_count (int, default 0).
- comments: id, article_id, body, is_published.
1) Behavior configuration in CommentsTable:
$this->addBehavior('CounterCache', [
'Articles' => [
'comment_count',
'published_comment_count' => [
'conditions' => ['Comments.is_published' => true]
]
]
]);
2) Populate existing data (production)
After deploying, rebuild counters:
bin/cake counter_cache --assoc Comments Articles
From that point on, counters will stay synchronized automatically.
Best practices and Common Mistakes
Here you have some best practices and common mistakes:
- Add indexes to foreign keys (comments.article_id) and fields used in conditions (comments.is_published) for large datasets.
- If you perform direct database imports (bypassing the ORM), remember to rebuild counters using bin/cake counter_cache or updateCounterCache().
- Counters defined using closures are not updated by updateCounterCache().
- If a record changes its foreign key (e.g. moving a comment from one article to another), CounterCache handles the increments and decrements safely.
This article is part of the CakeDC Advent Calendar 2025 (December 2th 2025)