CounterCacheBehavior.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since CakePHP(tm) v 3.0.0
  13. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  14. */
  15. namespace Cake\Model\Behavior;
  16. use Cake\Event\Event;
  17. use Cake\ORM\Association;
  18. use Cake\ORM\Behavior;
  19. use Cake\ORM\Entity;
  20. use Cake\ORM\Table;
  21. use Cake\Utility\Inflector;
  22. /**
  23. * CounterCache behavior
  24. *
  25. * Enables models to cache the amount of connections in a given relation.
  26. *
  27. * Examples with Post model belonging to User model
  28. *
  29. * Regular counter cache
  30. * {{{
  31. * [
  32. * 'User' => [
  33. * 'post_count'
  34. * ]
  35. * ]
  36. * }}}
  37. *
  38. * Counter cache with scope
  39. * {{{
  40. * [
  41. * 'User' => [
  42. * 'posts_published' => [
  43. * 'conditions' => [
  44. * 'published' => true
  45. * ]
  46. * ]
  47. * ]
  48. * ]
  49. * }}}
  50. *
  51. * Counter cache using custom find
  52. * {{{
  53. * [
  54. * 'User' => [
  55. * 'posts_published' => [
  56. * 'findType' => 'published' // Will be using findPublished()
  57. * ]
  58. * ]
  59. * ]
  60. * }}}
  61. *
  62. * Counter cache using lambda function returning the count
  63. * This is equivalent to example #2
  64. * {{{
  65. * [
  66. * 'User' => [
  67. * 'posts_published' => function (Event $event, Entity $entity, Table $table) {
  68. * $query = $table->find('all')->where([
  69. * 'published' => true,
  70. * 'user_id' => $entity->get('user_id')
  71. * ]);
  72. * return $query->count();
  73. * }
  74. * ]
  75. * ]
  76. * }}}
  77. *
  78. */
  79. class CounterCacheBehavior extends Behavior {
  80. /**
  81. * Keeping a reference to the table in order to,
  82. * be able to retrieve associations and fetch records for counting.
  83. *
  84. * @var array
  85. */
  86. protected $_table;
  87. /**
  88. * Constructor
  89. *
  90. * @param Table $table The table this behavior is attached to.
  91. * @param array $config The config for this behavior.
  92. */
  93. public function __construct(Table $table, array $config = []) {
  94. parent::__construct($table, $config);
  95. $this->_table = $table;
  96. }
  97. /**
  98. * afterSave callback.
  99. *
  100. * Makes sure to update counter cache when a new record is created or updated.
  101. *
  102. * @param Event $event
  103. * @param Entity $entity
  104. * @return void
  105. */
  106. public function afterSave(Event $event, Entity $entity) {
  107. $this->_processAssociations($event, $entity);
  108. }
  109. /**
  110. * afterDelete callback.
  111. *
  112. * Makes sure to update counter cache when a record is deleted.
  113. *
  114. * @param Event $event
  115. * @param Entity $entity
  116. * @return void
  117. */
  118. public function afterDelete(Event $event, Entity $entity) {
  119. $this->_processAssociations($event, $entity);
  120. }
  121. /**
  122. * Iterate all associations and update counter caches.
  123. *
  124. * @param Event $event
  125. * @param Entity $entity
  126. * @return void
  127. */
  128. protected function _processAssociations(Event $event, Entity $entity) {
  129. foreach ($this->_config as $assoc => $settings) {
  130. $assoc = $this->_table->association($assoc);
  131. $this->_processAssociation($event, $entity, $assoc, $settings);
  132. }
  133. }
  134. /**
  135. * Updates counter cache for a single association
  136. *
  137. * @param Event $event
  138. * @param Entity $entity
  139. * @param Association $assoc The association object
  140. * @param array $settings The settings for for counter cache for this association
  141. * @return void
  142. */
  143. protected function _processAssociation(Event $event, Entity $entity, Association $assoc, array $settings) {
  144. $foreignKeys = (array)$assoc->foreignKey();
  145. $primaryKeys = (array)$assoc->target()->primaryKey();
  146. $countConditions = $entity->extract($foreignKeys);
  147. $updateConditions = array_combine($primaryKeys, $countConditions);
  148. foreach ($settings as $field => $config) {
  149. if (is_int($field)) {
  150. $field = $config;
  151. $config = [];
  152. }
  153. if (is_callable($config)) {
  154. $count = $config($event, $entity, $this->_table);
  155. } else {
  156. $count = $this->_getCount($config, $countConditions);
  157. }
  158. $assoc->target()->updateAll([$field => $count], $updateConditions);
  159. }
  160. }
  161. /**
  162. * Fetches and returns the count for a single field in an association
  163. *
  164. * @param array $config The counter cache configuration for a single field
  165. * @param array $conditions Additional conditions given to the query
  166. * @return integer The number of relations matching the given config and conditions
  167. */
  168. protected function _getCount(array $config, array $conditions) {
  169. $findType = 'all';
  170. if (!empty($config['findType'])) {
  171. $findType = $config['findType'];
  172. unset($config['findType']);
  173. }
  174. if (!isset($config['conditions'])) {
  175. $config['conditions'] = [];
  176. }
  177. $config['conditions'] = array_merge($conditions, $config['conditions']);
  178. $query = $this->_table->find($findType, $config);
  179. return $query->count();
  180. }
  181. }