HasManyTest.php 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\ORM\Association;
  16. use Cake\Database\Expression\OrderByExpression;
  17. use Cake\Database\Expression\QueryExpression;
  18. use Cake\Database\Expression\TupleComparison;
  19. use Cake\Database\IdentifierQuoter;
  20. use Cake\Database\TypeMap;
  21. use Cake\Datasource\ConnectionManager;
  22. use Cake\ORM\Association;
  23. use Cake\ORM\Association\HasMany;
  24. use Cake\ORM\Entity;
  25. use Cake\TestSuite\TestCase;
  26. /**
  27. * Tests HasMany class
  28. */
  29. class HasManyTest extends TestCase
  30. {
  31. /**
  32. * Fixtures
  33. *
  34. * @var array
  35. */
  36. public $fixtures = [
  37. 'core.Comments',
  38. 'core.Articles',
  39. 'core.Authors',
  40. ];
  41. /**
  42. * Set up
  43. *
  44. * @return void
  45. */
  46. public function setUp()
  47. {
  48. parent::setUp();
  49. $this->author = $this->getTableLocator()->get('Authors', [
  50. 'schema' => [
  51. 'id' => ['type' => 'integer'],
  52. 'name' => ['type' => 'string'],
  53. '_constraints' => [
  54. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  55. ],
  56. ],
  57. ]);
  58. $connection = ConnectionManager::get('test');
  59. $this->article = $this->getMockBuilder('Cake\ORM\Table')
  60. ->setMethods(['find', 'deleteAll', 'delete'])
  61. ->setConstructorArgs([['alias' => 'Articles', 'table' => 'articles', 'connection' => $connection]])
  62. ->getMock();
  63. $this->article->setSchema([
  64. 'id' => ['type' => 'integer'],
  65. 'title' => ['type' => 'string'],
  66. 'author_id' => ['type' => 'integer'],
  67. '_constraints' => [
  68. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  69. ],
  70. ]);
  71. $this->articlesTypeMap = new TypeMap([
  72. 'Articles.id' => 'integer',
  73. 'id' => 'integer',
  74. 'Articles.title' => 'string',
  75. 'title' => 'string',
  76. 'Articles.author_id' => 'integer',
  77. 'author_id' => 'integer',
  78. 'Articles__id' => 'integer',
  79. 'Articles__title' => 'string',
  80. 'Articles__author_id' => 'integer',
  81. ]);
  82. $this->autoQuote = $connection->getDriver()->isAutoQuotingEnabled();
  83. }
  84. /**
  85. * Tests that foreignKey() returns the correct configured value
  86. *
  87. * @return void
  88. */
  89. public function testSetForeignKey()
  90. {
  91. $assoc = new HasMany('Articles', [
  92. 'sourceTable' => $this->author,
  93. ]);
  94. $this->assertEquals('author_id', $assoc->getForeignKey());
  95. $this->assertSame($assoc, $assoc->setForeignKey('another_key'));
  96. $this->assertEquals('another_key', $assoc->getForeignKey());
  97. }
  98. /**
  99. * Tests that foreignKey() returns the correct configured value
  100. *
  101. * @group deprecated
  102. * @return void
  103. */
  104. public function testForeignKey()
  105. {
  106. $this->deprecated(function () {
  107. $assoc = new HasMany('Articles', [
  108. 'sourceTable' => $this->author,
  109. ]);
  110. $this->assertEquals('author_id', $assoc->foreignKey());
  111. $this->assertEquals('another_key', $assoc->foreignKey('another_key'));
  112. $this->assertEquals('another_key', $assoc->foreignKey());
  113. });
  114. }
  115. /**
  116. * Test that foreignKey generation ignores database names in target table.
  117. *
  118. * @return void
  119. */
  120. public function testForeignKeyIgnoreDatabaseName()
  121. {
  122. $this->author->setTable('schema.authors');
  123. $assoc = new HasMany('Articles', [
  124. 'sourceTable' => $this->author,
  125. ]);
  126. $this->assertEquals('author_id', $assoc->getForeignKey());
  127. }
  128. /**
  129. * Tests that the association reports it can be joined
  130. *
  131. * @return void
  132. */
  133. public function testCanBeJoined()
  134. {
  135. $assoc = new HasMany('Test');
  136. $this->assertFalse($assoc->canBeJoined());
  137. }
  138. /**
  139. * Tests sort() method
  140. *
  141. * @group deprecated
  142. * @return void
  143. */
  144. public function testSort()
  145. {
  146. $this->deprecated(function () {
  147. $assoc = new HasMany('Test');
  148. $this->assertNull($assoc->sort());
  149. $assoc->sort(['id' => 'ASC']);
  150. $this->assertEquals(['id' => 'ASC'], $assoc->sort());
  151. });
  152. }
  153. /**
  154. * Tests setSort() method
  155. *
  156. * @return void
  157. */
  158. public function testSetSort()
  159. {
  160. $assoc = new HasMany('Test');
  161. $this->assertNull($assoc->getSort());
  162. $assoc->setSort(['id' => 'ASC']);
  163. $this->assertEquals(['id' => 'ASC'], $assoc->getSort());
  164. }
  165. /**
  166. * Tests requiresKeys() method
  167. *
  168. * @return void
  169. */
  170. public function testRequiresKeys()
  171. {
  172. $assoc = new HasMany('Test');
  173. $this->assertTrue($assoc->requiresKeys());
  174. $assoc->setStrategy(HasMany::STRATEGY_SUBQUERY);
  175. $this->assertFalse($assoc->requiresKeys());
  176. $assoc->setStrategy(HasMany::STRATEGY_SELECT);
  177. $this->assertTrue($assoc->requiresKeys());
  178. }
  179. /**
  180. * Tests that HasMany can't use the join strategy
  181. *
  182. * @return void
  183. */
  184. public function testStrategyFailure()
  185. {
  186. $this->expectException(\InvalidArgumentException::class);
  187. $this->expectExceptionMessage('Invalid strategy "join" was provided');
  188. $assoc = new HasMany('Test');
  189. $assoc->setStrategy(HasMany::STRATEGY_JOIN);
  190. }
  191. /**
  192. * Test the eager loader method with no extra options
  193. *
  194. * @return void
  195. */
  196. public function testEagerLoader()
  197. {
  198. $config = [
  199. 'sourceTable' => $this->author,
  200. 'targetTable' => $this->article,
  201. 'strategy' => 'select',
  202. ];
  203. $association = new HasMany('Articles', $config);
  204. $query = $this->article->query();
  205. $this->article->method('find')
  206. ->with('all')
  207. ->will($this->returnValue($query));
  208. $keys = [1, 2, 3, 4];
  209. $callable = $association->eagerLoader(compact('keys', 'query'));
  210. $row = ['Authors__id' => 1];
  211. $result = $callable($row);
  212. $this->assertArrayHasKey('Articles', $result);
  213. $this->assertEquals($row['Authors__id'], $result['Articles'][0]->author_id);
  214. $this->assertEquals($row['Authors__id'], $result['Articles'][1]->author_id);
  215. $row = ['Authors__id' => 2];
  216. $result = $callable($row);
  217. $this->assertArrayNotHasKey('Articles', $result);
  218. $row = ['Authors__id' => 3];
  219. $result = $callable($row);
  220. $this->assertArrayHasKey('Articles', $result);
  221. $this->assertEquals($row['Authors__id'], $result['Articles'][0]->author_id);
  222. $row = ['Authors__id' => 4];
  223. $result = $callable($row);
  224. $this->assertArrayNotHasKey('Articles', $result);
  225. }
  226. /**
  227. * Test the eager loader method with default query clauses
  228. *
  229. * @return void
  230. */
  231. public function testEagerLoaderWithDefaults()
  232. {
  233. $config = [
  234. 'sourceTable' => $this->author,
  235. 'targetTable' => $this->article,
  236. 'conditions' => ['Articles.published' => 'Y'],
  237. 'sort' => ['id' => 'ASC'],
  238. 'strategy' => 'select',
  239. ];
  240. $association = new HasMany('Articles', $config);
  241. $keys = [1, 2, 3, 4];
  242. $query = $this->article->query();
  243. $this->article->method('find')
  244. ->with('all')
  245. ->will($this->returnValue($query));
  246. $association->eagerLoader(compact('keys', 'query'));
  247. $expected = new QueryExpression(
  248. ['Articles.published' => 'Y', 'Articles.author_id IN' => $keys],
  249. $this->articlesTypeMap
  250. );
  251. $this->assertWhereClause($expected, $query);
  252. $expected = new OrderByExpression(['id' => 'ASC']);
  253. $this->assertOrderClause($expected, $query);
  254. }
  255. /**
  256. * Test the eager loader method with overridden query clauses
  257. *
  258. * @return void
  259. */
  260. public function testEagerLoaderWithOverrides()
  261. {
  262. $config = [
  263. 'sourceTable' => $this->author,
  264. 'targetTable' => $this->article,
  265. 'conditions' => ['Articles.published' => 'Y'],
  266. 'sort' => ['id' => 'ASC'],
  267. 'strategy' => 'select',
  268. ];
  269. $this->article->hasMany('Comments');
  270. $association = new HasMany('Articles', $config);
  271. $keys = [1, 2, 3, 4];
  272. /** @var \Cake\ORM\Query $query */
  273. $query = $this->article->query();
  274. $query->addDefaultTypes($this->article->Comments->getSource());
  275. $this->article->method('find')
  276. ->with('all')
  277. ->will($this->returnValue($query));
  278. $association->eagerLoader([
  279. 'conditions' => ['Articles.id !=' => 3],
  280. 'sort' => ['title' => 'DESC'],
  281. 'fields' => ['title', 'author_id'],
  282. 'contain' => ['Comments' => ['fields' => ['comment', 'article_id']]],
  283. 'keys' => $keys,
  284. 'query' => $query,
  285. ]);
  286. $expected = [
  287. 'Articles__title' => 'Articles.title',
  288. 'Articles__author_id' => 'Articles.author_id',
  289. ];
  290. $this->assertSelectClause($expected, $query);
  291. $expected = new QueryExpression(
  292. [
  293. 'Articles.published' => 'Y',
  294. 'Articles.id !=' => 3,
  295. 'Articles.author_id IN' => $keys,
  296. ],
  297. $query->getTypeMap()
  298. );
  299. $this->assertWhereClause($expected, $query);
  300. $expected = new OrderByExpression(['title' => 'DESC']);
  301. $this->assertOrderClause($expected, $query);
  302. $this->assertArrayHasKey('Comments', $query->getContain());
  303. }
  304. /**
  305. * Test that failing to add the foreignKey to the list of fields will throw an
  306. * exception
  307. *
  308. * @return void
  309. */
  310. public function testEagerLoaderFieldsException()
  311. {
  312. $this->expectException(\InvalidArgumentException::class);
  313. $this->expectExceptionMessage('You are required to select the "Articles.author_id"');
  314. $config = [
  315. 'sourceTable' => $this->author,
  316. 'targetTable' => $this->article,
  317. 'strategy' => 'select',
  318. ];
  319. $association = new HasMany('Articles', $config);
  320. $keys = [1, 2, 3, 4];
  321. $query = $this->article->query();
  322. $this->article->method('find')
  323. ->with('all')
  324. ->will($this->returnValue($query));
  325. $association->eagerLoader([
  326. 'fields' => ['id', 'title'],
  327. 'keys' => $keys,
  328. 'query' => $query,
  329. ]);
  330. }
  331. /**
  332. * Tests that eager loader accepts a queryBuilder option
  333. *
  334. * @return void
  335. */
  336. public function testEagerLoaderWithQueryBuilder()
  337. {
  338. $config = [
  339. 'sourceTable' => $this->author,
  340. 'targetTable' => $this->article,
  341. 'strategy' => 'select',
  342. ];
  343. $association = new HasMany('Articles', $config);
  344. $keys = [1, 2, 3, 4];
  345. /** @var \Cake\ORM\Query $query */
  346. $query = $this->article->query();
  347. $this->article->method('find')
  348. ->with('all')
  349. ->will($this->returnValue($query));
  350. $queryBuilder = function ($query) {
  351. return $query->select(['author_id'])->join('comments')->where(['comments.id' => 1]);
  352. };
  353. $association->eagerLoader(compact('keys', 'query', 'queryBuilder'));
  354. $expected = [
  355. 'Articles__author_id' => 'Articles.author_id',
  356. ];
  357. $this->assertSelectClause($expected, $query);
  358. $expected = [
  359. [
  360. 'type' => 'INNER',
  361. 'alias' => null,
  362. 'table' => 'comments',
  363. 'conditions' => new QueryExpression([], $query->getTypeMap()),
  364. ],
  365. ];
  366. $this->assertJoin($expected, $query);
  367. $expected = new QueryExpression(
  368. [
  369. 'Articles.author_id IN' => $keys,
  370. 'comments.id' => 1,
  371. ],
  372. $query->getTypeMap()
  373. );
  374. $this->assertWhereClause($expected, $query);
  375. }
  376. /**
  377. * Test the eager loader method with no extra options
  378. *
  379. * @return void
  380. */
  381. public function testEagerLoaderMultipleKeys()
  382. {
  383. $config = [
  384. 'sourceTable' => $this->author,
  385. 'targetTable' => $this->article,
  386. 'strategy' => 'select',
  387. 'foreignKey' => ['author_id', 'site_id'],
  388. ];
  389. $this->author->setPrimaryKey(['id', 'site_id']);
  390. $association = new HasMany('Articles', $config);
  391. $keys = [[1, 10], [2, 20], [3, 30], [4, 40]];
  392. $query = $this->getMockBuilder('Cake\ORM\Query')
  393. ->setMethods(['all', 'andWhere'])
  394. ->disableOriginalConstructor()
  395. ->getMock();
  396. $this->article->method('find')
  397. ->with('all')
  398. ->will($this->returnValue($query));
  399. $results = [
  400. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10],
  401. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20],
  402. ];
  403. $query->method('all')
  404. ->will($this->returnValue($results));
  405. $tuple = new TupleComparison(
  406. ['Articles.author_id', 'Articles.site_id'],
  407. $keys,
  408. [],
  409. 'IN'
  410. );
  411. $query->expects($this->once())->method('andWhere')
  412. ->with($tuple)
  413. ->will($this->returnSelf());
  414. $callable = $association->eagerLoader(compact('keys', 'query'));
  415. $row = ['Authors__id' => 2, 'Authors__site_id' => 10, 'username' => 'author 1'];
  416. $result = $callable($row);
  417. $row['Articles'] = [
  418. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10],
  419. ];
  420. $this->assertEquals($row, $result);
  421. $row = ['Authors__id' => 1, 'username' => 'author 2', 'Authors__site_id' => 20];
  422. $result = $callable($row);
  423. $row['Articles'] = [
  424. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20],
  425. ];
  426. $this->assertEquals($row, $result);
  427. }
  428. /**
  429. * Test cascading deletes.
  430. *
  431. * @return void
  432. */
  433. public function testCascadeDelete()
  434. {
  435. $config = [
  436. 'dependent' => true,
  437. 'sourceTable' => $this->author,
  438. 'targetTable' => $this->article,
  439. 'conditions' => ['Articles.is_active' => true],
  440. ];
  441. $association = new HasMany('Articles', $config);
  442. $this->article->expects($this->once())
  443. ->method('deleteAll')
  444. ->with([
  445. 'Articles.is_active' => true,
  446. 'Articles.author_id' => 1,
  447. ]);
  448. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  449. $association->cascadeDelete($entity);
  450. }
  451. /**
  452. * Test cascading delete with has many.
  453. *
  454. * @return void
  455. */
  456. public function testCascadeDeleteCallbacks()
  457. {
  458. $articles = $this->getTableLocator()->get('Articles');
  459. $config = [
  460. 'dependent' => true,
  461. 'sourceTable' => $this->author,
  462. 'targetTable' => $articles,
  463. 'conditions' => ['Articles.published' => 'Y'],
  464. 'cascadeCallbacks' => true,
  465. ];
  466. $association = new HasMany('Articles', $config);
  467. $author = new Entity(['id' => 1, 'name' => 'mark']);
  468. $this->assertTrue($association->cascadeDelete($author));
  469. $query = $articles->query()->where(['author_id' => 1]);
  470. $this->assertEquals(0, $query->count(), 'Cleared related rows');
  471. $query = $articles->query()->where(['author_id' => 3]);
  472. $this->assertEquals(1, $query->count(), 'other records left behind');
  473. }
  474. /**
  475. * Test that saveAssociated() ignores non entity values.
  476. *
  477. * @return void
  478. */
  479. public function testSaveAssociatedOnlyEntities()
  480. {
  481. $mock = $this->getMockBuilder('Cake\ORM\Table')
  482. ->setMethods(['saveAssociated'])
  483. ->disableOriginalConstructor()
  484. ->getMock();
  485. $config = [
  486. 'sourceTable' => $this->author,
  487. 'targetTable' => $mock,
  488. ];
  489. $entity = new Entity([
  490. 'username' => 'Mark',
  491. 'email' => 'mark@example.com',
  492. 'articles' => [
  493. ['title' => 'First Post'],
  494. new Entity(['title' => 'Second Post']),
  495. ],
  496. ]);
  497. $mock->expects($this->never())
  498. ->method('saveAssociated');
  499. $association = new HasMany('Articles', $config);
  500. $association->saveAssociated($entity);
  501. }
  502. /**
  503. * Tests that property is being set using the constructor options.
  504. *
  505. * @return void
  506. */
  507. public function testPropertyOption()
  508. {
  509. $config = ['propertyName' => 'thing_placeholder'];
  510. $association = new hasMany('Thing', $config);
  511. $this->assertEquals('thing_placeholder', $association->getProperty());
  512. }
  513. /**
  514. * Test that plugin names are omitted from property()
  515. *
  516. * @return void
  517. */
  518. public function testPropertyNoPlugin()
  519. {
  520. $mock = $this->getMockBuilder('Cake\ORM\Table')
  521. ->disableOriginalConstructor()
  522. ->getMock();
  523. $config = [
  524. 'sourceTable' => $this->author,
  525. 'targetTable' => $mock,
  526. ];
  527. $association = new HasMany('Contacts.Addresses', $config);
  528. $this->assertEquals('addresses', $association->getProperty());
  529. }
  530. /**
  531. * Test that the ValueBinder is reset when using strategy = Association::STRATEGY_SUBQUERY
  532. *
  533. * @return void
  534. */
  535. public function testValueBinderUpdateOnSubQueryStrategy()
  536. {
  537. $Authors = $this->getTableLocator()->get('Authors');
  538. $Authors->hasMany('Articles', [
  539. 'strategy' => Association::STRATEGY_SUBQUERY,
  540. ]);
  541. $query = $Authors->find();
  542. $authorsAndArticles = $query
  543. ->select([
  544. 'id',
  545. 'slug' => $query->func()->concat([
  546. '---',
  547. 'name' => 'identifier',
  548. ]),
  549. ])
  550. ->contain('Articles')
  551. ->where(['name' => 'mariano'])
  552. ->first();
  553. $this->assertCount(2, $authorsAndArticles->get('articles'));
  554. }
  555. /**
  556. * Assertion method for order by clause contents.
  557. *
  558. * @param array $expected The expected join clause.
  559. * @param \Cake\ORM\Query $query The query to check.
  560. * @return void
  561. */
  562. protected function assertJoin($expected, $query)
  563. {
  564. if ($this->autoQuote) {
  565. $driver = $query->getConnection()->getDriver();
  566. $quoter = new IdentifierQuoter($driver);
  567. foreach ($expected as &$join) {
  568. $join['table'] = $driver->quoteIdentifier($join['table']);
  569. if ($join['conditions']) {
  570. $quoter->quoteExpression($join['conditions']);
  571. }
  572. }
  573. }
  574. $this->assertEquals($expected, array_values($query->clause('join')));
  575. }
  576. /**
  577. * Assertion method for where clause contents.
  578. *
  579. * @param \Cake\Database\QueryExpression $expected The expected where clause.
  580. * @param \Cake\ORM\Query $query The query to check.
  581. * @return void
  582. */
  583. protected function assertWhereClause($expected, $query)
  584. {
  585. if ($this->autoQuote) {
  586. $quoter = new IdentifierQuoter($query->getConnection()->getDriver());
  587. $expected->traverse([$quoter, 'quoteExpression']);
  588. }
  589. $this->assertEquals($expected, $query->clause('where'));
  590. }
  591. /**
  592. * Assertion method for order by clause contents.
  593. *
  594. * @param \Cake\Database\QueryExpression $expected The expected where clause.
  595. * @param \Cake\ORM\Query $query The query to check.
  596. * @return void
  597. */
  598. protected function assertOrderClause($expected, $query)
  599. {
  600. if ($this->autoQuote) {
  601. $quoter = new IdentifierQuoter($query->getConnection()->getDriver());
  602. $quoter->quoteExpression($expected);
  603. }
  604. $this->assertEquals($expected, $query->clause('order'));
  605. }
  606. /**
  607. * Assertion method for select clause contents.
  608. *
  609. * @param array $expected Array of expected fields.
  610. * @param \Cake\ORM\Query $query The query to check.
  611. * @return void
  612. */
  613. protected function assertSelectClause($expected, $query)
  614. {
  615. if ($this->autoQuote) {
  616. $connection = $query->getConnection();
  617. foreach ($expected as $key => $value) {
  618. $expected[$connection->quoteIdentifier($key)] = $connection->quoteIdentifier($value);
  619. unset($expected[$key]);
  620. }
  621. }
  622. $this->assertEquals($expected, $query->clause('select'));
  623. }
  624. /**
  625. * Tests that unlinking calls the right methods
  626. *
  627. * @return void
  628. */
  629. public function testUnlinkSuccess()
  630. {
  631. $articles = $this->getTableLocator()->get('Articles');
  632. $assoc = $this->author->hasMany('Articles', [
  633. 'sourceTable' => $this->author,
  634. 'targetTable' => $articles,
  635. ]);
  636. $entity = $this->author->get(1, ['contain' => 'Articles']);
  637. $initial = $entity->articles;
  638. $this->assertCount(2, $initial);
  639. $assoc->unlink($entity, $entity->articles);
  640. $this->assertEmpty($entity->get('articles'), 'Property should be empty');
  641. $new = $this->author->get(2, ['contain' => 'Articles']);
  642. $this->assertCount(0, $new->articles, 'DB should be clean');
  643. $this->assertSame(4, $this->author->find()->count(), 'Authors should still exist');
  644. $this->assertSame(3, $articles->find()->count(), 'Articles should still exist');
  645. }
  646. /**
  647. * Tests that unlink with an empty array does nothing
  648. *
  649. * @return void
  650. */
  651. public function testUnlinkWithEmptyArray()
  652. {
  653. $articles = $this->getTableLocator()->get('Articles');
  654. $assoc = $this->author->hasMany('Articles', [
  655. 'sourceTable' => $this->author,
  656. 'targetTable' => $articles,
  657. ]);
  658. $entity = $this->author->get(1, ['contain' => 'Articles']);
  659. $initial = $entity->articles;
  660. $this->assertCount(2, $initial);
  661. $assoc->unlink($entity, []);
  662. $new = $this->author->get(1, ['contain' => 'Articles']);
  663. $this->assertCount(2, $new->articles, 'Articles should remain linked');
  664. $this->assertSame(4, $this->author->find()->count(), 'Authors should still exist');
  665. $this->assertSame(3, $articles->find()->count(), 'Articles should still exist');
  666. }
  667. /**
  668. * Tests that link only uses a single database transaction
  669. *
  670. * @return void
  671. */
  672. public function testLinkUsesSingleTransaction()
  673. {
  674. $articles = $this->getTableLocator()->get('Articles');
  675. $assoc = $this->author->hasMany('Articles', [
  676. 'sourceTable' => $this->author,
  677. 'targetTable' => $articles,
  678. ]);
  679. // Ensure author in fixture has zero associated articles
  680. $entity = $this->author->get(2, ['contain' => 'Articles']);
  681. $initial = $entity->articles;
  682. $this->assertCount(0, $initial);
  683. // Ensure that after each model is saved, we are still within a transaction.
  684. $listenerAfterSave = function ($e, $entity, $options) use ($articles) {
  685. $this->assertTrue(
  686. $articles->getConnection()->inTransaction(),
  687. 'Multiple transactions used to save associated models.'
  688. );
  689. };
  690. $articles->getEventManager()->on('Model.afterSave', $listenerAfterSave);
  691. $options = ['atomic' => false];
  692. $assoc->link($entity, $articles->find('all')->toArray(), $options);
  693. // Ensure that link was successful.
  694. $new = $this->author->get(2, ['contain' => 'Articles']);
  695. $this->assertCount(3, $new->articles);
  696. }
  697. /**
  698. * Test that saveAssociated() fails on non-empty, non-iterable value
  699. *
  700. * @return void
  701. */
  702. public function testSaveAssociatedNotEmptyNotIterable()
  703. {
  704. $this->expectException(\InvalidArgumentException::class);
  705. $this->expectExceptionMessage('Could not save comments, it cannot be traversed');
  706. $articles = $this->getTableLocator()->get('Articles');
  707. $association = $articles->hasMany('Comments', [
  708. 'saveStrategy' => HasMany::SAVE_APPEND,
  709. ]);
  710. $entity = $articles->newEntity();
  711. $entity->set('comments', 'oh noes');
  712. $association->saveAssociated($entity);
  713. }
  714. /**
  715. * Data provider for empty values.
  716. *
  717. * @return array
  718. */
  719. public function emptySetDataProvider()
  720. {
  721. return [
  722. [''],
  723. [false],
  724. [null],
  725. [[]],
  726. ];
  727. }
  728. /**
  729. * Test that saving empty sets with the `append` strategy does not
  730. * affect the associated records for not yet persisted parent entities.
  731. *
  732. * @dataProvider emptySetDataProvider
  733. * @param mixed $value Empty value.
  734. * @return void
  735. */
  736. public function testSaveAssociatedEmptySetWithAppendStrategyDoesNotAffectAssociatedRecordsOnCreate($value)
  737. {
  738. $articles = $this->getTableLocator()->get('Articles');
  739. $association = $articles->hasMany('Comments', [
  740. 'saveStrategy' => HasMany::SAVE_APPEND,
  741. ]);
  742. $comments = $association->find();
  743. $this->assertNotEmpty($comments);
  744. $entity = $articles->newEntity();
  745. $entity->set('comments', $value);
  746. $this->assertSame($entity, $association->saveAssociated($entity));
  747. $this->assertEquals($value, $entity->get('comments'));
  748. $this->assertEquals($comments, $association->find());
  749. }
  750. /**
  751. * Test that saving empty sets with the `append` strategy does not
  752. * affect the associated records for already persisted parent entities.
  753. *
  754. * @dataProvider emptySetDataProvider
  755. * @param mixed $value Empty value.
  756. * @return void
  757. */
  758. public function testSaveAssociatedEmptySetWithAppendStrategyDoesNotAffectAssociatedRecordsOnUpdate($value)
  759. {
  760. $articles = $this->getTableLocator()->get('Articles');
  761. $association = $articles->hasMany('Comments', [
  762. 'saveStrategy' => HasMany::SAVE_APPEND,
  763. ]);
  764. $entity = $articles->get(1, [
  765. 'contain' => ['Comments'],
  766. ]);
  767. $comments = $entity->get('comments');
  768. $this->assertNotEmpty($comments);
  769. $entity->set('comments', $value);
  770. $this->assertSame($entity, $association->saveAssociated($entity));
  771. $this->assertEquals($value, $entity->get('comments'));
  772. $entity = $articles->get(1, [
  773. 'contain' => ['Comments'],
  774. ]);
  775. $this->assertEquals($comments, $entity->get('comments'));
  776. }
  777. /**
  778. * Test that saving empty sets with the `replace` strategy does not
  779. * affect the associated records for not yet persisted parent entities.
  780. *
  781. * @dataProvider emptySetDataProvider
  782. * @param mixed $value Empty value.
  783. * @return void
  784. */
  785. public function testSaveAssociatedEmptySetWithReplaceStrategyDoesNotAffectAssociatedRecordsOnCreate($value)
  786. {
  787. $articles = $this->getTableLocator()->get('Articles');
  788. $association = $articles->hasMany('Comments', [
  789. 'saveStrategy' => HasMany::SAVE_REPLACE,
  790. ]);
  791. $comments = $association->find();
  792. $this->assertNotEmpty($comments);
  793. $entity = $articles->newEntity();
  794. $entity->set('comments', $value);
  795. $this->assertSame($entity, $association->saveAssociated($entity));
  796. $this->assertEquals($value, $entity->get('comments'));
  797. $this->assertEquals($comments, $association->find());
  798. }
  799. /**
  800. * Test that saving empty sets with the `replace` strategy does remove
  801. * the associated records for already persisted parent entities.
  802. *
  803. * @dataProvider emptySetDataProvider
  804. * @param mixed $value Empty value.
  805. * @return void
  806. */
  807. public function testSaveAssociatedEmptySetWithReplaceStrategyRemovesAssociatedRecordsOnUpdate($value)
  808. {
  809. $articles = $this->getTableLocator()->get('Articles');
  810. $association = $articles->hasMany('Comments', [
  811. 'saveStrategy' => HasMany::SAVE_REPLACE,
  812. ]);
  813. $entity = $articles->get(1, [
  814. 'contain' => ['Comments'],
  815. ]);
  816. $comments = $entity->get('comments');
  817. $this->assertNotEmpty($comments);
  818. $entity->set('comments', $value);
  819. $this->assertSame($entity, $association->saveAssociated($entity));
  820. $this->assertEquals([], $entity->get('comments'));
  821. $entity = $articles->get(1, [
  822. 'contain' => ['Comments'],
  823. ]);
  824. $this->assertEmpty($entity->get('comments'));
  825. }
  826. /**
  827. * Tests that providing an invalid strategy throws an exception
  828. *
  829. * @return void
  830. */
  831. public function testInvalidSaveStrategy()
  832. {
  833. $this->expectException(\InvalidArgumentException::class);
  834. $articles = $this->getTableLocator()->get('Articles');
  835. $association = $articles->hasMany('Comments');
  836. $association->setSaveStrategy('anotherThing');
  837. }
  838. /**
  839. * Tests saveStrategy
  840. *
  841. * @return void
  842. */
  843. public function testSetSaveStrategy()
  844. {
  845. $articles = $this->getTableLocator()->get('Articles');
  846. $association = $articles->hasMany('Comments');
  847. $this->assertSame($association, $association->setSaveStrategy(HasMany::SAVE_REPLACE));
  848. $this->assertSame(HasMany::SAVE_REPLACE, $association->getSaveStrategy());
  849. }
  850. /**
  851. * Tests saveStrategy
  852. *
  853. * @group deprecated
  854. * @return void
  855. */
  856. public function testSaveStrategy()
  857. {
  858. $this->deprecated(function () {
  859. $articles = $this->getTableLocator()->get('Articles');
  860. $association = $articles->hasMany('Comments');
  861. $this->assertSame(HasMany::SAVE_REPLACE, $association->saveStrategy(HasMany::SAVE_REPLACE));
  862. $this->assertSame(HasMany::SAVE_REPLACE, $association->saveStrategy());
  863. });
  864. }
  865. /**
  866. * Test that save works with replace saveStrategy and are not deleted once they are not null
  867. *
  868. * @return void
  869. */
  870. public function testSaveReplaceSaveStrategy()
  871. {
  872. $authors = $this->getTableLocator()->get('Authors');
  873. $authors->hasMany('Articles', ['saveStrategy' => HasMany::SAVE_REPLACE]);
  874. $entity = $authors->newEntity([
  875. 'name' => 'mylux',
  876. 'articles' => [
  877. ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
  878. ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
  879. ['title' => 'One more random post', 'body' => 'The cake is forever'],
  880. ],
  881. ], ['associated' => ['Articles']]);
  882. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  883. $sizeArticles = count($entity->articles);
  884. $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  885. $articleId = $entity->articles[0]->id;
  886. unset($entity->articles[0]);
  887. $entity->setDirty('articles', true);
  888. $authors->save($entity, ['associated' => ['Articles']]);
  889. $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  890. $this->assertTrue($authors->Articles->exists(['id' => $articleId]));
  891. }
  892. /**
  893. * Test that save works with replace saveStrategy conditions
  894. *
  895. * @return void
  896. */
  897. public function testSaveReplaceSaveStrategyClosureConditions()
  898. {
  899. $authors = $this->getTableLocator()->get('Authors');
  900. $authors->hasMany('Articles')
  901. ->setDependent(true)
  902. ->setSaveStrategy('replace')
  903. ->setConditions(function () {
  904. return ['published' => 'Y'];
  905. });
  906. $entity = $authors->newEntity([
  907. 'name' => 'mylux',
  908. 'articles' => [
  909. ['title' => 'Not matching conditions', 'body' => '', 'published' => 'N'],
  910. ['title' => 'Random Post', 'body' => 'The cake is nice', 'published' => 'Y'],
  911. ['title' => 'Another Random Post', 'body' => 'The cake is yummy', 'published' => 'Y'],
  912. ['title' => 'One more random post', 'body' => 'The cake is forever', 'published' => 'Y'],
  913. ],
  914. ], ['associated' => ['Articles']]);
  915. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  916. $sizeArticles = count($entity->articles);
  917. // Should be one fewer because of conditions.
  918. $this->assertSame($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  919. $articleId = $entity->articles[0]->id;
  920. unset($entity->articles[0], $entity->articles[1]);
  921. $entity->setDirty('articles', true);
  922. $authors->save($entity, ['associated' => ['Articles']]);
  923. $this->assertSame($sizeArticles - 2, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  924. // Should still exist because it doesn't match the association conditions.
  925. $articles = $this->getTableLocator()->get('Articles');
  926. $this->assertTrue($articles->exists(['id' => $articleId]));
  927. }
  928. /**
  929. * Test that save works with replace saveStrategy, replacing the already persisted entities even if no new entities are passed
  930. *
  931. * @return void
  932. */
  933. public function testSaveReplaceSaveStrategyNotAdding()
  934. {
  935. $authors = $this->getTableLocator()->get('Authors');
  936. $authors->hasMany('Articles', ['saveStrategy' => 'replace']);
  937. $entity = $authors->newEntity([
  938. 'name' => 'mylux',
  939. 'articles' => [
  940. ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
  941. ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
  942. ['title' => 'One more random post', 'body' => 'The cake is forever'],
  943. ],
  944. ], ['associated' => ['Articles']]);
  945. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  946. $sizeArticles = count($entity->articles);
  947. $this->assertCount($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']]));
  948. $entity->set('articles', []);
  949. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  950. $this->assertCount(0, $authors->Articles->find('all')->where(['author_id' => $entity['id']]));
  951. }
  952. /**
  953. * Test that save works with append saveStrategy not deleting or setting null anything
  954. *
  955. * @return void
  956. */
  957. public function testSaveAppendSaveStrategy()
  958. {
  959. $authors = $this->getTableLocator()->get('Authors');
  960. $authors->hasMany('Articles', ['saveStrategy' => 'append']);
  961. $entity = $authors->newEntity([
  962. 'name' => 'mylux',
  963. 'articles' => [
  964. ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
  965. ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
  966. ['title' => 'One more random post', 'body' => 'The cake is forever'],
  967. ],
  968. ], ['associated' => ['Articles']]);
  969. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  970. $sizeArticles = count($entity->articles);
  971. $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  972. $articleId = $entity->articles[0]->id;
  973. unset($entity->articles[0]);
  974. $entity->setDirty('articles', true);
  975. $authors->save($entity, ['associated' => ['Articles']]);
  976. $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  977. $this->assertTrue($authors->Articles->exists(['id' => $articleId]));
  978. }
  979. /**
  980. * Test that save has append as the default save strategy
  981. *
  982. * @return void
  983. */
  984. public function testSaveDefaultSaveStrategy()
  985. {
  986. $authors = $this->getTableLocator()->get('Authors');
  987. $authors->hasMany('Articles', ['saveStrategy' => HasMany::SAVE_APPEND]);
  988. $this->assertEquals(HasMany::SAVE_APPEND, $authors->getAssociation('articles')->getSaveStrategy());
  989. }
  990. /**
  991. * Test that the associated entities are unlinked and deleted when they are dependent
  992. *
  993. * @return void
  994. */
  995. public function testSaveReplaceSaveStrategyDependent()
  996. {
  997. $authors = $this->getTableLocator()->get('Authors');
  998. $authors->hasMany('Articles', ['saveStrategy' => HasMany::SAVE_REPLACE, 'dependent' => true]);
  999. $entity = $authors->newEntity([
  1000. 'name' => 'mylux',
  1001. 'articles' => [
  1002. ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
  1003. ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
  1004. ['title' => 'One more random post', 'body' => 'The cake is forever'],
  1005. ],
  1006. ], ['associated' => ['Articles']]);
  1007. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  1008. $sizeArticles = count($entity->articles);
  1009. $this->assertEquals($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  1010. $articleId = $entity->articles[0]->id;
  1011. unset($entity->articles[0]);
  1012. $entity->setDirty('articles', true);
  1013. $authors->save($entity, ['associated' => ['Articles']]);
  1014. $this->assertEquals($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  1015. $this->assertFalse($authors->Articles->exists(['id' => $articleId]));
  1016. }
  1017. /**
  1018. * Test that the associated entities are unlinked and deleted when they are dependent
  1019. * when associated entities array is indexed by string keys
  1020. *
  1021. * @return void
  1022. */
  1023. public function testSaveReplaceSaveStrategyDependentWithStringKeys()
  1024. {
  1025. $authors = $this->getTableLocator()->get('Authors');
  1026. $authors->hasMany('Articles', ['saveStrategy' => HasMany::SAVE_REPLACE, 'dependent' => true]);
  1027. $entity = $authors->newEntity([
  1028. 'name' => 'mylux',
  1029. 'articles' => [
  1030. ['title' => 'One Random Post', 'body' => 'The cake is not a lie'],
  1031. ['title' => 'Another Random Post', 'body' => 'The cake is nice'],
  1032. ['title' => 'One more random post', 'body' => 'The cake is forever'],
  1033. ],
  1034. ], ['associated' => ['Articles']]);
  1035. $entity = $authors->saveOrFail($entity, ['associated' => ['Articles']]);
  1036. $sizeArticles = count($entity->articles);
  1037. $this->assertSame($sizeArticles, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  1038. $articleId = $entity->articles[0]->id;
  1039. $entity->articles = [
  1040. 'one' => $entity->articles[1],
  1041. 'two' => $entity->articles[2],
  1042. ];
  1043. $authors->saveOrFail($entity, ['associated' => ['Articles']]);
  1044. $this->assertSame($sizeArticles - 1, $authors->Articles->find('all')->where(['author_id' => $entity['id']])->count());
  1045. $this->assertFalse($authors->Articles->exists(['id' => $articleId]));
  1046. }
  1047. /**
  1048. * Test that the associated entities are unlinked and deleted when they are dependent
  1049. *
  1050. * In the future this should change and apply the finder.
  1051. *
  1052. * @return void
  1053. */
  1054. public function testSaveReplaceSaveStrategyDependentWithConditions()
  1055. {
  1056. $this->getTableLocator()->clear();
  1057. $this->setAppNamespace('TestApp');
  1058. $authors = $this->getTableLocator()->get('Authors');
  1059. $authors->hasMany('Articles', [
  1060. 'finder' => 'published',
  1061. 'saveStrategy' => HasMany::SAVE_REPLACE,
  1062. 'dependent' => true,
  1063. ]);
  1064. $articles = $authors->Articles->getTarget();
  1065. $articles->updateAll(['published' => 'N'], ['author_id' => 1, 'title' => 'Third Article']);
  1066. $entity = $authors->get(1, ['contain' => ['Articles']]);
  1067. $data = [
  1068. 'name' => 'updated',
  1069. 'articles' => [
  1070. ['title' => 'First Article', 'body' => 'New First', 'published' => 'N'],
  1071. ],
  1072. ];
  1073. $entity = $authors->patchEntity($entity, $data, ['associated' => ['Articles']]);
  1074. $entity = $authors->save($entity, ['associated' => ['Articles']]);
  1075. // Should only have one article left as we 'replaced' the others.
  1076. $this->assertCount(1, $entity->articles);
  1077. $this->assertCount(1, $authors->Articles->find()->toArray());
  1078. $others = $articles->find('all')
  1079. ->where(['Articles.author_id' => 1])
  1080. ->orderAsc('title')
  1081. ->toArray();
  1082. $this->assertCount(
  1083. 1,
  1084. $others,
  1085. 'Record not matching condition should stay. But does not'
  1086. );
  1087. $this->assertSame('First Article', $others[0]->title);
  1088. }
  1089. /**
  1090. * Test that the associated entities are unlinked and deleted when they have a not nullable foreign key
  1091. *
  1092. * @return void
  1093. */
  1094. public function testSaveReplaceSaveStrategyNotNullable()
  1095. {
  1096. $articles = $this->getTableLocator()->get('Articles');
  1097. $articles->hasMany('Comments', ['saveStrategy' => HasMany::SAVE_REPLACE]);
  1098. $article = $articles->newEntity([
  1099. 'title' => 'Bakeries are sky rocketing',
  1100. 'body' => 'All because of cake',
  1101. 'comments' => [
  1102. [
  1103. 'user_id' => 1,
  1104. 'comment' => 'That is true!',
  1105. ],
  1106. [
  1107. 'user_id' => 2,
  1108. 'comment' => 'Of course',
  1109. ],
  1110. ],
  1111. ], ['associated' => ['Comments']]);
  1112. $article = $articles->save($article, ['associated' => ['Comments']]);
  1113. $commentId = $article->comments[0]->id;
  1114. $sizeComments = count($article->comments);
  1115. $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
  1116. $this->assertTrue($articles->Comments->exists(['id' => $commentId]));
  1117. unset($article->comments[0]);
  1118. $article->setDirty('comments', true);
  1119. $article = $articles->save($article, ['associated' => ['Comments']]);
  1120. $this->assertEquals($sizeComments - 1, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
  1121. $this->assertFalse($articles->Comments->exists(['id' => $commentId]));
  1122. }
  1123. /**
  1124. * Test that the associated entities are unlinked and deleted when they have a not nullable foreign key
  1125. *
  1126. * @return void
  1127. */
  1128. public function testSaveReplaceSaveStrategyAdding()
  1129. {
  1130. $articles = $this->getTableLocator()->get('Articles');
  1131. $articles->hasMany('Comments', ['saveStrategy' => HasMany::SAVE_REPLACE]);
  1132. $article = $articles->newEntity([
  1133. 'title' => 'Bakeries are sky rocketing',
  1134. 'body' => 'All because of cake',
  1135. 'comments' => [
  1136. [
  1137. 'user_id' => 1,
  1138. 'comment' => 'That is true!',
  1139. ],
  1140. [
  1141. 'user_id' => 2,
  1142. 'comment' => 'Of course',
  1143. ],
  1144. ],
  1145. ], ['associated' => ['Comments']]);
  1146. $article = $articles->save($article, ['associated' => ['Comments']]);
  1147. $commentId = $article->comments[0]->id;
  1148. $sizeComments = count($article->comments);
  1149. $articleId = $article->id;
  1150. $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
  1151. $this->assertTrue($articles->Comments->exists(['id' => $commentId]));
  1152. unset($article->comments[0]);
  1153. $article->comments[] = $articles->Comments->newEntity([
  1154. 'user_id' => 1,
  1155. 'comment' => 'new comment',
  1156. ]);
  1157. $article->setDirty('comments', true);
  1158. $article = $articles->save($article, ['associated' => ['Comments']]);
  1159. $this->assertEquals($sizeComments, $articles->Comments->find('all')->where(['article_id' => $article->id])->count());
  1160. $this->assertFalse($articles->Comments->exists(['id' => $commentId]));
  1161. $this->assertTrue($articles->Comments->exists(['comment' => 'new comment', 'article_id' => $articleId]));
  1162. }
  1163. /**
  1164. * Tests that dependent, non-cascading deletes are using the association
  1165. * conditions for deleting associated records.
  1166. *
  1167. * @return void
  1168. */
  1169. public function testHasManyNonCascadingUnlinkDeleteUsesAssociationConditions()
  1170. {
  1171. $Articles = $this->getTableLocator()->get('Articles');
  1172. $Comments = $Articles->hasMany('Comments', [
  1173. 'dependent' => true,
  1174. 'cascadeCallbacks' => false,
  1175. 'saveStrategy' => HasMany::SAVE_REPLACE,
  1176. 'conditions' => [
  1177. 'Comments.published' => 'Y',
  1178. ],
  1179. ]);
  1180. $article = $Articles->newEntity([
  1181. 'title' => 'Title',
  1182. 'body' => 'Body',
  1183. 'comments' => [
  1184. [
  1185. 'user_id' => 1,
  1186. 'comment' => 'First comment',
  1187. 'published' => 'Y',
  1188. ],
  1189. [
  1190. 'user_id' => 1,
  1191. 'comment' => 'Second comment',
  1192. 'published' => 'Y',
  1193. ],
  1194. ],
  1195. ]);
  1196. $article = $Articles->save($article);
  1197. $this->assertNotEmpty($article);
  1198. $comment3 = $Comments->getTarget()->newEntity([
  1199. 'article_id' => $article->get('id'),
  1200. 'user_id' => 1,
  1201. 'comment' => 'Third comment',
  1202. 'published' => 'N',
  1203. ]);
  1204. $comment3 = $Comments->getTarget()->save($comment3);
  1205. $this->assertNotEmpty($comment3);
  1206. $this->assertEquals(3, $Comments->getTarget()->find()->where(['Comments.article_id' => $article->get('id')])->count());
  1207. unset($article->comments[1]);
  1208. $article->setDirty('comments', true);
  1209. $article = $Articles->save($article);
  1210. $this->assertNotEmpty($article);
  1211. // Given the association condition of `'Comments.published' => 'Y'`,
  1212. // it is expected that only one of the three linked comments are
  1213. // actually being deleted, as only one of them matches the
  1214. // association condition.
  1215. $this->assertEquals(2, $Comments->getTarget()->find()->where(['Comments.article_id' => $article->get('id')])->count());
  1216. }
  1217. /**
  1218. * Tests that non-dependent, non-cascading deletes are using the association
  1219. * conditions for updating associated records.
  1220. *
  1221. * @return void
  1222. */
  1223. public function testHasManyNonDependentNonCascadingUnlinkUpdateUsesAssociationConditions()
  1224. {
  1225. $Authors = $this->getTableLocator()->get('Authors');
  1226. $Authors->associations()->removeAll();
  1227. $Articles = $Authors->hasMany('Articles', [
  1228. 'dependent' => false,
  1229. 'cascadeCallbacks' => false,
  1230. 'saveStrategy' => HasMany::SAVE_REPLACE,
  1231. 'conditions' => [
  1232. 'Articles.published' => 'Y',
  1233. ],
  1234. ]);
  1235. $author = $Authors->newEntity([
  1236. 'name' => 'Name',
  1237. 'articles' => [
  1238. [
  1239. 'title' => 'First article',
  1240. 'body' => 'First article',
  1241. 'published' => 'Y',
  1242. ],
  1243. [
  1244. 'title' => 'Second article',
  1245. 'body' => 'Second article',
  1246. 'published' => 'Y',
  1247. ],
  1248. ],
  1249. ]);
  1250. $author = $Authors->save($author);
  1251. $this->assertNotEmpty($author);
  1252. $article3 = $Articles->getTarget()->newEntity([
  1253. 'author_id' => $author->get('id'),
  1254. 'title' => 'Third article',
  1255. 'body' => 'Third article',
  1256. 'published' => 'N',
  1257. ]);
  1258. $article3 = $Articles->getTarget()->save($article3);
  1259. $this->assertNotEmpty($article3);
  1260. $this->assertEquals(3, $Articles->getTarget()->find()->where(['Articles.author_id' => $author->get('id')])->count());
  1261. $article2 = $author->articles[1];
  1262. unset($author->articles[1]);
  1263. $author->setDirty('articles', true);
  1264. $author = $Authors->save($author);
  1265. $this->assertNotEmpty($author);
  1266. // Given the association condition of `'Articles.published' => 'Y'`,
  1267. // it is expected that only one of the three linked articles are
  1268. // actually being unlinked (nulled), as only one of them matches the
  1269. // association condition.
  1270. $this->assertEquals(2, $Articles->getTarget()->find()->where(['Articles.author_id' => $author->get('id')])->count());
  1271. $this->assertNull($Articles->get($article2->get('id'))->get('author_id'));
  1272. $this->assertEquals($author->get('id'), $Articles->get($article3->get('id'))->get('author_id'));
  1273. }
  1274. }