Often times web applications need to display counts of related objects. For example, when showing a list of articles you may want to display how many comments it has. Or when showing a user you might want to show how many friends/followers she has. The CounterCache behavior is intended for these situations. CounterCache will update a field in the associated models assigned in the options when it is invoked. The fields should exist in the database and be of the type INT.
You enable the CounterCache behavior like any other behavior, but it won’t do anything until you configure some relations and the field counts that should be stored on each of them. Using our example below, we could cache the comment count for each article with the following:
class CommentsTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('CounterCache', [
'Articles' => ['comment_count']
]);
}
}
Note
The column comment_count
should exist in the articles
table.
The CounterCache configuration should be a map of relation names and the specific configuration for that relation.
As you see you need to add the behavior on the “other side” of the association
where you actually want the field to be updated. In this example the behavior
is added to the CommentsTable
even though it updates the comment_count
field in the ArticlesTable
.
The counter’s value will be updated each time an entity is saved or deleted. The counter will not be updated when you
save the entity without changing data or
use updateAll()
or
use deleteAll()
or
execute SQL you have written
If you need to keep a cached counter for less than all of the related records, you can supply additional conditions or finder methods to generate a counter value:
// Use a specific find method.
// In this case find(published)
$this->addBehavior('CounterCache', [
'Articles' => [
'comment_count' => [
'finder' => 'published',
],
],
]);
If you don’t have a custom finder method you can provide an array of conditions to find records instead:
$this->addBehavior('CounterCache', [
'Articles' => [
'comment_count' => [
'conditions' => ['Comments.spam' => false],
],
],
]);
If you want CounterCache to update multiple fields, for example both showing a conditional count and a basic count you can add these fields in the array:
$this->addBehavior('CounterCache', [
'Articles' => ['comment_count',
'published_comment_count' => [
'finder' => 'published',
],
],
]);
If you want to calculate the CounterCache field value on your own, you can set
the ignoreDirty
option to true
.
This will prevent the field from being recalculated if you’ve set it dirty
before:
$this->addBehavior('CounterCache', [
'Articles' => [
'comment_count' => [
'ignoreDirty' => true,
],
],
]);
Lastly, if a custom finder and conditions are not suitable you can provide a callback function. Your function must return the count value to be stored:
$this->addBehavior('CounterCache', [
'Articles' => [
'rating_avg' => function ($event, $entity, $table, $original) {
return 4.5;
}
],
]);
Your function can return false
to skip updating the counter column, or
a SelectQuery
object that produced the count value. If you return a SelectQuery
object, your query will be used as a subquery in the update statement. The
$table
parameter refers to the table object holding the behavior (not the
target relation) for convenience. The callback is invoked at least once with
$original
set to false
. If the entity-update changes the association
then the callback is invoked a second time with true
, the return value
then updates the counter of the previously associated item.
Note
The CounterCache behavior works for belongsTo
associations only. For
example for “Comments belongsTo Articles”, you need to add the CounterCache
behavior to the CommentsTable
in order to generate comment_count
for
Articles table.
Changed in version 5.1.2: As of CakePHP 5.1.2, the counter cache values are updated using a single query using sub-queries, instead of separate queries, to fetch the count and update a record. If required you can disable the use of sub-queries by setting useSubQuery key to false in the config [‘Articles’ => [‘comment_count’ => [‘useSubQuery’ => false]]
It is possible to use the CounterCache behavior in a belongsToMany
association.
First, you need to add the through
and cascadeCallbacks
options to the
belongsToMany
association:
'through' => 'CommentsArticles',
'cascadeCallbacks' => true
Also see Using the ‘through’ Option how to configure a custom join table.
The CommentsArticles
is the name of the junction table classname.
If you don’t have it you should create it with the bake CLI tool.
In this src/Model/Table/CommentsArticlesTable.php
you then need to add the behavior
with the same code as described above.:
$this->addBehavior('CounterCache', [
'Articles' => ['comments_count'],
]);
Finally clear all caches with bin/cake cache clear_all
and try it out.