CounterCacheBehaviorTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\ORM\Behavior;
  17. use Cake\Database\Driver\Sqlserver;
  18. use Cake\Datasource\ConnectionManager;
  19. use Cake\Datasource\EntityInterface;
  20. use Cake\Event\EventInterface;
  21. use Cake\ORM\Entity;
  22. use Cake\ORM\Table;
  23. use Cake\TestSuite\TestCase;
  24. use Exception;
  25. use TestApp\Model\Table\PublishedPostsTable;
  26. /**
  27. * CounterCacheBehavior test case
  28. */
  29. class CounterCacheBehaviorTest extends TestCase
  30. {
  31. /**
  32. * @var \TestApp\Model\Table\PublishedPostsTable
  33. */
  34. protected $post;
  35. /**
  36. * @var \TestApp\Model\Table\PublishedPostsTable
  37. */
  38. protected $user;
  39. /**
  40. * @var \TestApp\Model\Table\PublishedPostsTable
  41. */
  42. protected $category;
  43. /**
  44. * @var \TestApp\Model\Table\PublishedPostsTable
  45. */
  46. protected $comment;
  47. /**
  48. * @var \TestApp\Model\Table\PublishedPostsTable
  49. */
  50. protected $userCategoryPosts;
  51. /**
  52. * @var \Cake\Datasource\ConnectionInterface
  53. */
  54. protected $connection;
  55. /**
  56. * Fixture
  57. *
  58. * @var list<string>
  59. */
  60. protected array $fixtures = [
  61. 'core.CounterCacheCategories',
  62. 'core.CounterCachePosts',
  63. 'core.CounterCacheComments',
  64. 'core.CounterCacheUsers',
  65. 'core.CounterCacheUserCategoryPosts',
  66. ];
  67. /**
  68. * setup
  69. */
  70. public function setUp(): void
  71. {
  72. parent::setUp();
  73. $this->connection = ConnectionManager::get('test');
  74. $this->user = $this->getTableLocator()->get('Users', [
  75. 'table' => 'counter_cache_users',
  76. 'connection' => $this->connection,
  77. ]);
  78. $this->category = $this->getTableLocator()->get('Categories', [
  79. 'table' => 'counter_cache_categories',
  80. 'connection' => $this->connection,
  81. ]);
  82. $this->comment = $this->getTableLocator()->get('Comments', [
  83. 'alias' => 'Comment',
  84. 'table' => 'counter_cache_comments',
  85. 'connection' => $this->connection,
  86. ]);
  87. $this->post = new PublishedPostsTable([
  88. 'alias' => 'Post',
  89. 'table' => 'counter_cache_posts',
  90. 'connection' => $this->connection,
  91. ]);
  92. $this->userCategoryPosts = new Table([
  93. 'alias' => 'UserCategoryPosts',
  94. 'table' => 'counter_cache_user_category_posts',
  95. 'connection' => $this->connection,
  96. ]);
  97. }
  98. /**
  99. * teardown
  100. */
  101. public function tearDown(): void
  102. {
  103. parent::tearDown();
  104. unset($this->user, $this->post);
  105. }
  106. /**
  107. * Testing simple counter caching when adding a record
  108. */
  109. public function testAdd(): void
  110. {
  111. $this->skipIf(
  112. $this->connection->getDriver() instanceof Sqlserver,
  113. 'This test fails sporadically in SQLServer'
  114. );
  115. $this->post->belongsTo('Users');
  116. $this->post->addBehavior('CounterCache', [
  117. 'Users' => [
  118. 'post_count',
  119. ],
  120. ]);
  121. $before = $this->_getUser();
  122. $entity = $this->_getEntity();
  123. $this->post->save($entity);
  124. $after = $this->_getUser();
  125. $this->assertSame(2, $before->get('post_count'));
  126. $this->assertSame(3, $after->get('post_count'));
  127. }
  128. /**
  129. * Testing simple counter caching when adding a record
  130. */
  131. public function testAddIgnore(): void
  132. {
  133. $this->post->belongsTo('Users');
  134. $this->post->addBehavior('CounterCache', [
  135. 'Users' => [
  136. 'post_count',
  137. ],
  138. ]);
  139. $before = $this->_getUser();
  140. $entity = $this->_getEntity();
  141. $this->post->save($entity, ['ignoreCounterCache' => true]);
  142. $after = $this->_getUser();
  143. $this->assertSame(2, $before->get('post_count'));
  144. $this->assertSame(2, $after->get('post_count'));
  145. }
  146. /**
  147. * Testing simple counter caching when adding a record
  148. */
  149. public function testAddScope(): void
  150. {
  151. $this->post->belongsTo('Users');
  152. $this->post->addBehavior('CounterCache', [
  153. 'Users' => [
  154. 'posts_published' => [
  155. 'conditions' => [
  156. 'published' => true,
  157. ],
  158. ],
  159. ],
  160. ]);
  161. $before = $this->_getUser();
  162. $entity = $this->_getEntity()->set('published', true);
  163. $this->post->save($entity);
  164. $after = $this->_getUser();
  165. $this->assertSame(1, $before->get('posts_published'));
  166. $this->assertSame(2, $after->get('posts_published'));
  167. }
  168. public function testSaveWithNullForeignKey(): void
  169. {
  170. $this->comment->belongsTo('Users');
  171. $this->comment->addBehavior('CounterCache', [
  172. 'Users' => [
  173. 'comment_count',
  174. ],
  175. ]);
  176. $entity = new Entity([
  177. 'title' => 'Orphan comment',
  178. 'user_id' => null,
  179. ]);
  180. $this->comment->saveOrFail($entity);
  181. $this->assertTrue(true);
  182. }
  183. /**
  184. * Testing simple counter caching when deleting a record
  185. */
  186. public function testDelete(): void
  187. {
  188. $this->post->belongsTo('Users');
  189. $this->post->addBehavior('CounterCache', [
  190. 'Users' => [
  191. 'post_count',
  192. ],
  193. ]);
  194. $before = $this->_getUser();
  195. $post = $this->post->find('all')->first();
  196. $this->post->delete($post);
  197. $after = $this->_getUser();
  198. $this->assertSame(2, $before->get('post_count'));
  199. $this->assertSame(1, $after->get('post_count'));
  200. }
  201. /**
  202. * Testing simple counter caching when deleting a record
  203. */
  204. public function testDeleteIgnore(): void
  205. {
  206. $this->post->belongsTo('Users');
  207. $this->post->addBehavior('CounterCache', [
  208. 'Users' => [
  209. 'post_count',
  210. ],
  211. ]);
  212. $before = $this->_getUser();
  213. $post = $this->post->find('all')
  214. ->first();
  215. $this->post->delete($post, ['ignoreCounterCache' => true]);
  216. $after = $this->_getUser();
  217. $this->assertSame(2, $before->get('post_count'));
  218. $this->assertSame(2, $after->get('post_count'));
  219. }
  220. /**
  221. * Testing update simple counter caching when updating a record association
  222. */
  223. public function testUpdate(): void
  224. {
  225. $this->post->belongsTo('Users');
  226. $this->post->belongsTo('Categories');
  227. $this->post->addBehavior('CounterCache', [
  228. 'Users' => [
  229. 'post_count',
  230. ],
  231. 'Categories' => [
  232. 'post_count',
  233. ],
  234. ]);
  235. $user1 = $this->_getUser(1);
  236. $user2 = $this->_getUser(2);
  237. $category1 = $this->_getCategory(1);
  238. $category2 = $this->_getCategory(2);
  239. $post = $this->post->find('all')->first();
  240. $this->assertSame(2, $user1->get('post_count'));
  241. $this->assertSame(1, $user2->get('post_count'));
  242. $this->assertSame(1, $category1->get('post_count'));
  243. $this->assertSame(2, $category2->get('post_count'));
  244. $entity = $this->post->patchEntity($post, ['user_id' => 2, 'category_id' => 2]);
  245. $this->post->save($entity);
  246. $user1 = $this->_getUser(1);
  247. $user2 = $this->_getUser(2);
  248. $category1 = $this->_getCategory(1);
  249. $category2 = $this->_getCategory(2);
  250. $this->assertSame(1, $user1->get('post_count'));
  251. $this->assertSame(2, $user2->get('post_count'));
  252. $this->assertSame(0, $category1->get('post_count'));
  253. $this->assertSame(3, $category2->get('post_count'));
  254. $entity = $this->post->patchEntity($post, ['user_id' => null, 'category_id' => null]);
  255. $this->post->save($entity);
  256. $user2 = $this->_getUser(2);
  257. $category2 = $this->_getCategory(2);
  258. $this->assertSame(1, $user2->get('post_count'));
  259. $this->assertSame(2, $category2->get('post_count'));
  260. $entity = $this->post->patchEntity($post, ['user_id' => 2, 'category_id' => 2]);
  261. $this->post->save($entity);
  262. $user2 = $this->_getUser(2);
  263. $category2 = $this->_getCategory(2);
  264. $this->assertSame(2, $user2->get('post_count'));
  265. $this->assertSame(3, $category2->get('post_count'));
  266. }
  267. /**
  268. * Testing counter cache with custom find
  269. */
  270. public function testCustomFind(): void
  271. {
  272. $this->post->belongsTo('Users');
  273. $this->post->addBehavior('CounterCache', [
  274. 'Users' => [
  275. 'posts_published' => [
  276. 'finder' => 'published',
  277. ],
  278. ],
  279. ]);
  280. $before = $this->_getUser();
  281. $entity = $this->_getEntity()->set('published', true);
  282. $this->post->save($entity);
  283. $after = $this->_getUser();
  284. $this->assertSame(1, $before->get('posts_published'));
  285. $this->assertSame(2, $after->get('posts_published'));
  286. }
  287. public function testCustomFindWithoutSubquery(): void
  288. {
  289. $this->post->belongsTo('Users');
  290. $this->post->addBehavior('CounterCache', [
  291. 'Users' => [
  292. 'posts_published' => [
  293. 'finder' => 'published',
  294. 'useSubQuery' => false,
  295. ],
  296. ],
  297. ]);
  298. $before = $this->_getUser();
  299. $entity = $this->_getEntity()->set('published', true);
  300. $this->post->save($entity);
  301. $after = $this->_getUser();
  302. $this->assertSame(1, $before->get('posts_published'));
  303. $this->assertSame(2, $after->get('posts_published'));
  304. }
  305. /**
  306. * Testing counter cache with lambda returning number
  307. */
  308. public function testLambdaNumber(): void
  309. {
  310. $this->post->belongsTo('Users');
  311. $table = $this->post;
  312. $entity = $this->_getEntity();
  313. $this->post->addBehavior('CounterCache', [
  314. 'Users' => [
  315. 'posts_published' => function (EventInterface $orgEvent, EntityInterface $orgEntity, Table $orgTable) use ($entity, $table) {
  316. $this->assertSame($orgTable, $table);
  317. $this->assertSame($orgEntity, $entity);
  318. return 2;
  319. },
  320. ],
  321. ]);
  322. $before = $this->_getUser();
  323. $this->post->save($entity);
  324. $after = $this->_getUser();
  325. $this->assertSame(1, $before->get('posts_published'));
  326. $this->assertSame(2, $after->get('posts_published'));
  327. }
  328. /**
  329. * Testing counter cache with lambda returning false
  330. */
  331. public function testLambdaFalse(): void
  332. {
  333. $this->post->belongsTo('Users');
  334. $table = $this->post;
  335. $entity = $this->_getEntity();
  336. $this->post->addBehavior('CounterCache', [
  337. 'Users' => [
  338. 'posts_published' => function (EventInterface $orgEvent, EntityInterface $orgEntity, Table $orgTable) use ($entity, $table) {
  339. $this->assertSame($orgTable, $table);
  340. $this->assertSame($orgEntity, $entity);
  341. return false;
  342. },
  343. ],
  344. ]);
  345. $before = $this->_getUser();
  346. $this->post->save($entity);
  347. $after = $this->_getUser();
  348. $this->assertSame(1, $before->get('posts_published'));
  349. $this->assertSame(1, $after->get('posts_published'));
  350. }
  351. /**
  352. * Testing counter cache with lambda returning number and changing of related ID
  353. */
  354. public function testLambdaNumberUpdate(): void
  355. {
  356. $this->post->belongsTo('Users');
  357. $table = $this->post;
  358. $entity = $this->_getEntity();
  359. $this->post->addBehavior('CounterCache', [
  360. 'Users' => [
  361. 'posts_published' => function (EventInterface $orgEvent, EntityInterface $orgEntity, Table $orgTable, $original) use ($entity, $table) {
  362. $this->assertSame($orgTable, $table);
  363. $this->assertSame($orgEntity, $entity);
  364. if (!$original) {
  365. return 2;
  366. }
  367. return 1;
  368. },
  369. ],
  370. ]);
  371. $this->post->save($entity);
  372. $between = $this->_getUser();
  373. $entity->user_id = 2;
  374. $this->post->save($entity);
  375. $afterUser1 = $this->_getUser(1);
  376. $afterUser2 = $this->_getUser(2);
  377. $this->assertSame(2, $between->get('posts_published'));
  378. $this->assertSame(1, $afterUser1->get('posts_published'));
  379. $this->assertSame(2, $afterUser2->get('posts_published'));
  380. }
  381. /**
  382. * Testing counter cache with lambda returning a subquery
  383. */
  384. public function testLambdaSubquery(): void
  385. {
  386. $this->post->belongsTo('Users');
  387. $this->post->addBehavior('CounterCache', [
  388. 'Users' => [
  389. 'posts_published' => function (EventInterface $event, EntityInterface $entity, Table $table) {
  390. return $table->getConnection()->selectQuery(4);
  391. },
  392. ],
  393. ]);
  394. $before = $this->_getUser();
  395. $entity = $this->_getEntity();
  396. $this->post->save($entity);
  397. $after = $this->_getUser();
  398. $this->assertSame(1, $before->get('posts_published'));
  399. $this->assertSame(4, $after->get('posts_published'));
  400. }
  401. /**
  402. * Testing multiple counter cache when adding a record
  403. */
  404. public function testMultiple(): void
  405. {
  406. $this->post->belongsTo('Users');
  407. $this->post->addBehavior('CounterCache', [
  408. 'Users' => [
  409. 'post_count',
  410. 'posts_published' => [
  411. 'conditions' => [
  412. 'published' => true,
  413. ],
  414. ],
  415. ],
  416. ]);
  417. $before = $this->_getUser();
  418. $entity = $this->_getEntity()->set('published', true);
  419. $this->post->save($entity);
  420. $after = $this->_getUser();
  421. $this->assertSame(1, $before->get('posts_published'));
  422. $this->assertSame(2, $after->get('posts_published'));
  423. $this->assertSame(2, $before->get('post_count'));
  424. $this->assertSame(3, $after->get('post_count'));
  425. }
  426. /**
  427. * Tests to see that the binding key configuration is respected.
  428. */
  429. public function testBindingKey(): void
  430. {
  431. $this->post->hasMany('UserCategoryPosts', [
  432. 'bindingKey' => ['category_id', 'user_id'],
  433. 'foreignKey' => ['category_id', 'user_id'],
  434. ]);
  435. $this->post->getAssociation('UserCategoryPosts')->setTarget($this->userCategoryPosts);
  436. $this->post->addBehavior('CounterCache', [
  437. 'UserCategoryPosts' => ['post_count'],
  438. ]);
  439. $before = $this->userCategoryPosts->find()
  440. ->where(['user_id' => 1, 'category_id' => 2])
  441. ->first();
  442. $entity = $this->_getEntity()->set('category_id', 2);
  443. $this->post->save($entity);
  444. $after = $this->userCategoryPosts->find()
  445. ->where(['user_id' => 1, 'category_id' => 2])
  446. ->first();
  447. $this->assertSame(1, $before->get('post_count'));
  448. $this->assertSame(2, $after->get('post_count'));
  449. }
  450. /**
  451. * Testing the ignore if dirty option
  452. */
  453. public function testIgnoreDirty(): void
  454. {
  455. $this->post->belongsTo('Users');
  456. $this->comment->belongsTo('Users');
  457. $this->post->addBehavior('CounterCache', [
  458. 'Users' => [
  459. 'post_count' => [
  460. 'ignoreDirty' => true,
  461. ],
  462. 'comment_count' => [
  463. 'ignoreDirty' => true,
  464. ],
  465. ],
  466. ]);
  467. $user = $this->_getUser(1);
  468. $this->assertSame(2, $user->get('post_count'));
  469. $this->assertSame(2, $user->get('comment_count'));
  470. $this->assertSame(1, $user->get('posts_published'));
  471. $post = $this->post->find('all')
  472. ->contain('Users')
  473. ->where(['title' => 'Rock and Roll'])
  474. ->first();
  475. $post = $this->post->patchEntity($post, [
  476. 'posts_published' => true,
  477. 'user' => [
  478. 'id' => 1,
  479. 'post_count' => 10,
  480. 'comment_count' => 10,
  481. ],
  482. ]);
  483. $this->post->save($post);
  484. $user = $this->_getUser(1);
  485. $this->assertSame(10, $user->get('post_count'));
  486. $this->assertSame(10, $user->get('comment_count'));
  487. $this->assertSame(1, $user->get('posts_published'));
  488. }
  489. /**
  490. * Testing the ignore if dirty option with just one field set to ignoreDirty
  491. */
  492. public function testIgnoreDirtyMixed(): void
  493. {
  494. $this->post->belongsTo('Users');
  495. $this->comment->belongsTo('Users');
  496. $this->post->addBehavior('CounterCache', [
  497. 'Users' => [
  498. 'post_count' => [
  499. 'ignoreDirty' => true,
  500. ],
  501. ],
  502. ]);
  503. $user = $this->_getUser(1);
  504. $this->assertSame(2, $user->get('post_count'));
  505. $this->assertSame(2, $user->get('comment_count'));
  506. $this->assertSame(1, $user->get('posts_published'));
  507. $post = $this->post->find('all')
  508. ->contain('Users')
  509. ->where(['title' => 'Rock and Roll'])
  510. ->first();
  511. $post = $this->post->patchEntity($post, [
  512. 'posts_published' => true,
  513. 'user' => [
  514. 'id' => 1,
  515. 'post_count' => 10,
  516. ],
  517. ]);
  518. $this->post->save($post);
  519. $user = $this->_getUser(1);
  520. $this->assertSame(10, $user->get('post_count'));
  521. $this->assertSame(2, $user->get('comment_count'));
  522. $this->assertSame(1, $user->get('posts_published'));
  523. }
  524. public function testUpdateCounterCache(): void
  525. {
  526. $this->post->belongsTo('Users');
  527. $this->post->addBehavior('CounterCache', [
  528. 'Users' => [
  529. 'post_count',
  530. 'dummy' => function () {
  531. throw new Exception('Closures are never called by "updateCounterCache()"');
  532. },
  533. ],
  534. ]);
  535. $this->user->updateAll(['post_count' => 0], []);
  536. $user = $this->_getUser(1);
  537. $this->assertSame(0, $user->get('post_count'));
  538. $this->post->updateCounterCache('Users');
  539. $user = $this->_getUser(1);
  540. $this->assertSame(2, $user->get('post_count'));
  541. $user = $this->_getUser(2);
  542. $this->assertSame(1, $user->get('post_count'));
  543. $this->user->updateAll(['post_count' => 0], []);
  544. $this->post->updateCounterCache(limit: 1, page: 2);
  545. $user = $this->_getUser(1);
  546. $this->assertSame(0, $user->get('post_count'));
  547. $user = $this->_getUser(2);
  548. $this->assertSame(1, $user->get('post_count'));
  549. }
  550. /**
  551. * Get a new Entity
  552. */
  553. protected function _getEntity(): Entity
  554. {
  555. return new Entity([
  556. 'title' => 'Test 123',
  557. 'user_id' => 1,
  558. ]);
  559. }
  560. /**
  561. * Returns entity for user
  562. */
  563. protected function _getUser(int $id = 1): Entity
  564. {
  565. return $this->user->get($id);
  566. }
  567. /**
  568. * Returns entity for category
  569. */
  570. protected function _getCategory(int $id = 1): Entity
  571. {
  572. return $this->category->find('all')->where(['id' => $id])->first();
  573. }
  574. }