BelongsToManyTest.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  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 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\ORM\Association;
  16. use Cake\Database\Expression\IdentifierExpression;
  17. use Cake\Database\Expression\QueryExpression;
  18. use Cake\Database\Expression\TupleComparison;
  19. use Cake\Database\TypeMap;
  20. use Cake\Datasource\ConnectionManager;
  21. use Cake\ORM\Association\BelongsToMany;
  22. use Cake\ORM\Entity;
  23. use Cake\ORM\Query;
  24. use Cake\ORM\Table;
  25. use Cake\ORM\TableRegistry;
  26. use Cake\TestSuite\TestCase;
  27. /**
  28. * Tests BelongsToMany class
  29. *
  30. */
  31. class BelongsToManyTest extends TestCase
  32. {
  33. /**
  34. * Set up
  35. *
  36. * @return void
  37. */
  38. public function setUp()
  39. {
  40. parent::setUp();
  41. $this->tag = $this->getMock(
  42. 'Cake\ORM\Table',
  43. ['find', 'delete'],
  44. [['alias' => 'Tags', 'table' => 'tags']]
  45. );
  46. $this->tag->schema([
  47. 'id' => ['type' => 'integer'],
  48. 'name' => ['type' => 'string'],
  49. '_constraints' => [
  50. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  51. ]
  52. ]);
  53. $this->article = $this->getMock(
  54. 'Cake\ORM\Table',
  55. ['find', 'delete'],
  56. [['alias' => 'Articles', 'table' => 'articles']]
  57. );
  58. $this->article->schema([
  59. 'id' => ['type' => 'integer'],
  60. 'name' => ['type' => 'string'],
  61. '_constraints' => [
  62. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  63. ]
  64. ]);
  65. TableRegistry::set('Articles', $this->article);
  66. TableRegistry::get('ArticlesTags', [
  67. 'table' => 'articles_tags',
  68. 'schema' => [
  69. 'article_id' => ['type' => 'integer'],
  70. 'tag_id' => ['type' => 'integer'],
  71. '_constraints' => [
  72. 'primary' => ['type' => 'primary', 'columns' => ['article_id', 'tag_id']]
  73. ]
  74. ]
  75. ]);
  76. $this->tagsTypeMap = new TypeMap([
  77. 'Tags.id' => 'integer',
  78. 'id' => 'integer',
  79. 'Tags.name' => 'string',
  80. 'name' => 'string',
  81. ]);
  82. $this->articlesTagsTypeMap = new TypeMap([
  83. 'ArticlesTags.article_id' => 'integer',
  84. 'article_id' => 'integer',
  85. 'ArticlesTags.tag_id' => 'integer',
  86. 'tag_id' => 'integer',
  87. ]);
  88. }
  89. /**
  90. * Tear down
  91. *
  92. * @return void
  93. */
  94. public function tearDown()
  95. {
  96. parent::tearDown();
  97. TableRegistry::clear();
  98. }
  99. /**
  100. * Tests that the association reports it can be joined
  101. *
  102. * @return void
  103. */
  104. public function testCanBeJoined()
  105. {
  106. $assoc = new BelongsToMany('Test');
  107. $this->assertFalse($assoc->canBeJoined());
  108. }
  109. /**
  110. * Tests sort() method
  111. *
  112. * @return void
  113. */
  114. public function testSort()
  115. {
  116. $assoc = new BelongsToMany('Test');
  117. $this->assertNull($assoc->sort());
  118. $assoc->sort(['id' => 'ASC']);
  119. $this->assertEquals(['id' => 'ASC'], $assoc->sort());
  120. }
  121. /**
  122. * Tests requiresKeys() method
  123. *
  124. * @return void
  125. */
  126. public function testRequiresKeys()
  127. {
  128. $assoc = new BelongsToMany('Test');
  129. $this->assertTrue($assoc->requiresKeys());
  130. $assoc->strategy(BelongsToMany::STRATEGY_SUBQUERY);
  131. $this->assertFalse($assoc->requiresKeys());
  132. $assoc->strategy(BelongsToMany::STRATEGY_SELECT);
  133. $this->assertTrue($assoc->requiresKeys());
  134. }
  135. /**
  136. * Tests that BelongsToMany can't use the join strategy
  137. *
  138. * @expectedException \InvalidArgumentException
  139. * @expectedExceptionMessage Invalid strategy "join" was provided
  140. * @return void
  141. */
  142. public function testStrategyFailure()
  143. {
  144. $assoc = new BelongsToMany('Test');
  145. $assoc->strategy(BelongsToMany::STRATEGY_JOIN);
  146. }
  147. /**
  148. * Tests the junction method
  149. *
  150. * @return void
  151. */
  152. public function testJunction()
  153. {
  154. $assoc = new BelongsToMany('Test', [
  155. 'sourceTable' => $this->article,
  156. 'targetTable' => $this->tag
  157. ]);
  158. $junction = $assoc->junction();
  159. $this->assertInstanceOf('Cake\ORM\Table', $junction);
  160. $this->assertEquals('ArticlesTags', $junction->alias());
  161. $this->assertEquals('articles_tags', $junction->table());
  162. $this->assertSame($this->article, $junction->association('Articles')->target());
  163. $this->assertSame($this->tag, $junction->association('Tags')->target());
  164. $belongsTo = '\Cake\ORM\Association\BelongsTo';
  165. $this->assertInstanceOf($belongsTo, $junction->association('Articles'));
  166. $this->assertInstanceOf($belongsTo, $junction->association('Tags'));
  167. $this->assertSame($junction, $this->tag->association('ArticlesTags')->target());
  168. $this->assertSame($this->article, $this->tag->association('Articles')->target());
  169. $hasMany = '\Cake\ORM\Association\HasMany';
  170. $belongsToMany = '\Cake\ORM\Association\BelongsToMany';
  171. $this->assertInstanceOf($belongsToMany, $this->tag->association('Articles'));
  172. $this->assertInstanceOf($hasMany, $this->tag->association('ArticlesTags'));
  173. $this->assertSame($junction, $assoc->junction());
  174. $junction2 = TableRegistry::get('Foos');
  175. $assoc->junction($junction2);
  176. $this->assertSame($junction2, $assoc->junction());
  177. $assoc->junction('ArticlesTags');
  178. $this->assertSame($junction, $assoc->junction());
  179. }
  180. /**
  181. * Tests it is possible to set the table name for the join table
  182. *
  183. * @return void
  184. */
  185. public function testJunctionWithDefaultTableName()
  186. {
  187. $assoc = new BelongsToMany('Test', [
  188. 'sourceTable' => $this->article,
  189. 'targetTable' => $this->tag,
  190. 'joinTable' => 'tags_articles'
  191. ]);
  192. $junction = $assoc->junction();
  193. $this->assertEquals('TagsArticles', $junction->alias());
  194. $this->assertEquals('tags_articles', $junction->table());
  195. }
  196. /**
  197. * Tests saveStrategy
  198. *
  199. * @return void
  200. */
  201. public function testSaveStrategy()
  202. {
  203. $assoc = new BelongsToMany('Test');
  204. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->saveStrategy());
  205. $assoc->saveStrategy(BelongsToMany::SAVE_APPEND);
  206. $this->assertEquals(BelongsToMany::SAVE_APPEND, $assoc->saveStrategy());
  207. $assoc->saveStrategy(BelongsToMany::SAVE_REPLACE);
  208. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->saveStrategy());
  209. }
  210. /**
  211. * Tests that it is possible to pass the saveAssociated strategy in the constructor
  212. *
  213. * @return void
  214. */
  215. public function testSaveStrategyInOptions()
  216. {
  217. $assoc = new BelongsToMany('Test', ['saveStrategy' => BelongsToMany::SAVE_APPEND]);
  218. $this->assertEquals(BelongsToMany::SAVE_APPEND, $assoc->saveStrategy());
  219. }
  220. /**
  221. * Tests that passing an invalid strategy will throw an exception
  222. *
  223. * @expectedException \InvalidArgumentException
  224. * @expectedExceptionMessage Invalid save strategy "depsert"
  225. * @return void
  226. */
  227. public function testSaveStrategyInvalid()
  228. {
  229. $assoc = new BelongsToMany('Test', ['saveStrategy' => 'depsert']);
  230. }
  231. /**
  232. * Test cascading deletes.
  233. *
  234. * @return void
  235. */
  236. public function testCascadeDelete()
  237. {
  238. $articleTag = $this->getMock('Cake\ORM\Table', ['deleteAll'], []);
  239. $config = [
  240. 'sourceTable' => $this->article,
  241. 'targetTable' => $this->tag,
  242. 'sort' => ['id' => 'ASC'],
  243. ];
  244. $association = new BelongsToMany('Tags', $config);
  245. $association->junction($articleTag);
  246. $this->article
  247. ->association($articleTag->alias())
  248. ->conditions(['click_count' => 3]);
  249. $articleTag->expects($this->once())
  250. ->method('deleteAll')
  251. ->with([
  252. 'click_count' => 3,
  253. 'article_id' => 1
  254. ]);
  255. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  256. $association->cascadeDelete($entity);
  257. }
  258. /**
  259. * Test cascading deletes with dependent=false
  260. *
  261. * @return void
  262. */
  263. public function testCascadeDeleteDependent()
  264. {
  265. $articleTag = $this->getMock('Cake\ORM\Table', ['delete', 'deleteAll'], []);
  266. $config = [
  267. 'sourceTable' => $this->article,
  268. 'targetTable' => $this->tag,
  269. 'dependent' => false,
  270. 'sort' => ['id' => 'ASC'],
  271. ];
  272. $association = new BelongsToMany('Tags', $config);
  273. $association->junction($articleTag);
  274. $this->article
  275. ->association($articleTag->alias())
  276. ->conditions(['click_count' => 3]);
  277. $articleTag->expects($this->never())
  278. ->method('deleteAll');
  279. $articleTag->expects($this->never())
  280. ->method('delete');
  281. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  282. $association->cascadeDelete($entity);
  283. }
  284. /**
  285. * Test cascading deletes with callbacks.
  286. *
  287. * @return void
  288. */
  289. public function testCascadeDeleteWithCallbacks()
  290. {
  291. $articleTag = $this->getMock('Cake\ORM\Table', ['find', 'delete'], []);
  292. $config = [
  293. 'sourceTable' => $this->article,
  294. 'targetTable' => $this->tag,
  295. 'cascadeCallbacks' => true,
  296. ];
  297. $association = new BelongsToMany('Tag', $config);
  298. $association->junction($articleTag);
  299. $this->article
  300. ->association($articleTag->alias())
  301. ->conditions(['click_count' => 3]);
  302. $articleTagOne = new Entity(['article_id' => 1, 'tag_id' => 2]);
  303. $articleTagTwo = new Entity(['article_id' => 1, 'tag_id' => 4]);
  304. $iterator = new \ArrayIterator([
  305. $articleTagOne,
  306. $articleTagTwo
  307. ]);
  308. $query = $this->getMock('\Cake\ORM\Query', [], [], '', false);
  309. $query->expects($this->at(0))
  310. ->method('where')
  311. ->with(['click_count' => 3])
  312. ->will($this->returnSelf());
  313. $query->expects($this->at(1))
  314. ->method('where')
  315. ->with(['article_id' => 1])
  316. ->will($this->returnSelf());
  317. $query->expects($this->any())
  318. ->method('getIterator')
  319. ->will($this->returnValue($iterator));
  320. $articleTag->expects($this->once())
  321. ->method('find')
  322. ->will($this->returnValue($query));
  323. $articleTag->expects($this->at(1))
  324. ->method('delete')
  325. ->with($articleTagOne, []);
  326. $articleTag->expects($this->at(2))
  327. ->method('delete')
  328. ->with($articleTagTwo, []);
  329. $articleTag->expects($this->never())
  330. ->method('deleteAll');
  331. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  332. $association->cascadeDelete($entity);
  333. }
  334. /**
  335. * Test linking entities having a non persisted source entity
  336. *
  337. * @expectedException \InvalidArgumentException
  338. * @expectedExceptionMessage Source entity needs to be persisted before proceeding
  339. * @return void
  340. */
  341. public function testLinkWithNotPersistedSource()
  342. {
  343. $config = [
  344. 'sourceTable' => $this->article,
  345. 'targetTable' => $this->tag,
  346. 'joinTable' => 'tags_articles'
  347. ];
  348. $assoc = new BelongsToMany('Test', $config);
  349. $entity = new Entity(['id' => 1]);
  350. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  351. $assoc->link($entity, $tags);
  352. }
  353. /**
  354. * Test liking entities having a non persited target entity
  355. *
  356. * @expectedException \InvalidArgumentException
  357. * @expectedExceptionMessage Cannot link not persisted entities
  358. * @return void
  359. */
  360. public function testLinkWithNotPersistedTarget()
  361. {
  362. $config = [
  363. 'sourceTable' => $this->article,
  364. 'targetTable' => $this->tag,
  365. 'joinTable' => 'tags_articles'
  366. ];
  367. $assoc = new BelongsToMany('Test', $config);
  368. $entity = new Entity(['id' => 1], ['markNew' => false]);
  369. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  370. $assoc->link($entity, $tags);
  371. }
  372. /**
  373. * Tests that liking entities will validate data and pass on to _saveLinks
  374. *
  375. * @return void
  376. */
  377. public function testLinkSuccess()
  378. {
  379. $connection = ConnectionManager::get('test');
  380. $joint = $this->getMock(
  381. '\Cake\ORM\Table',
  382. ['save'],
  383. [['alias' => 'ArticlesTags', 'connection' => $connection]]
  384. );
  385. $config = [
  386. 'sourceTable' => $this->article,
  387. 'targetTable' => $this->tag,
  388. 'through' => $joint,
  389. 'joinTable' => 'tags_articles'
  390. ];
  391. $assoc = new BelongsToMany('Test', $config);
  392. $opts = ['markNew' => false];
  393. $entity = new Entity(['id' => 1], $opts);
  394. $tags = [new Entity(['id' => 2], $opts), new Entity(['id' => 3], $opts)];
  395. $saveOptions = ['foo' => 'bar'];
  396. $joint->expects($this->at(0))
  397. ->method('save')
  398. ->will($this->returnCallback(function ($e, $opts) use ($entity) {
  399. $expected = ['article_id' => 1, 'tag_id' => 2];
  400. $this->assertEquals($expected, $e->toArray());
  401. $this->assertEquals(['foo' => 'bar'], $opts);
  402. $this->assertTrue($e->isNew());
  403. return $entity;
  404. }));
  405. $joint->expects($this->at(1))
  406. ->method('save')
  407. ->will($this->returnCallback(function ($e, $opts) use ($entity) {
  408. $expected = ['article_id' => 1, 'tag_id' => 3];
  409. $this->assertEquals($expected, $e->toArray());
  410. $this->assertEquals(['foo' => 'bar'], $opts);
  411. $this->assertTrue($e->isNew());
  412. return $entity;
  413. }));
  414. $this->assertTrue($assoc->link($entity, $tags, $saveOptions));
  415. $this->assertSame($entity->test, $tags);
  416. }
  417. /**
  418. * Test liking entities having a non persited source entity
  419. *
  420. * @expectedException \InvalidArgumentException
  421. * @expectedExceptionMessage Source entity needs to be persisted before proceeding
  422. * @return void
  423. */
  424. public function testUnlinkWithNotPersistedSource()
  425. {
  426. $config = [
  427. 'sourceTable' => $this->article,
  428. 'targetTable' => $this->tag,
  429. 'joinTable' => 'tags_articles'
  430. ];
  431. $assoc = new BelongsToMany('Test', $config);
  432. $entity = new Entity(['id' => 1]);
  433. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  434. $assoc->unlink($entity, $tags);
  435. }
  436. /**
  437. * Test liking entities having a non persited target entity
  438. *
  439. * @expectedException \InvalidArgumentException
  440. * @expectedExceptionMessage Cannot link not persisted entities
  441. * @return void
  442. */
  443. public function testUnlinkWithNotPersistedTarget()
  444. {
  445. $config = [
  446. 'sourceTable' => $this->article,
  447. 'targetTable' => $this->tag,
  448. 'joinTable' => 'tags_articles'
  449. ];
  450. $assoc = new BelongsToMany('Test', $config);
  451. $entity = new Entity(['id' => 1], ['markNew' => false]);
  452. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  453. $assoc->unlink($entity, $tags);
  454. }
  455. /**
  456. * Tests that unlinking calls the right methods
  457. *
  458. * @return void
  459. */
  460. public function testUnlinkSuccess()
  461. {
  462. $connection = ConnectionManager::get('test');
  463. $joint = $this->getMock(
  464. '\Cake\ORM\Table',
  465. ['delete', 'find'],
  466. [['alias' => 'ArticlesTags', 'connection' => $connection]]
  467. );
  468. $config = [
  469. 'sourceTable' => $this->article,
  470. 'targetTable' => $this->tag,
  471. 'through' => $joint,
  472. 'joinTable' => 'tags_articles'
  473. ];
  474. $assoc = $this->article->belongsToMany('Test', $config);
  475. $assoc->junction();
  476. $this->article->association('ArticlesTags')
  477. ->conditions(['foo' => 1]);
  478. $query1 = $this->getMock('\Cake\ORM\Query', [], [$connection, $joint]);
  479. $query2 = $this->getMock('\Cake\ORM\Query', [], [$connection, $joint]);
  480. $joint->expects($this->at(0))->method('find')
  481. ->with('all')
  482. ->will($this->returnValue($query1));
  483. $joint->expects($this->at(1))->method('find')
  484. ->with('all')
  485. ->will($this->returnValue($query2));
  486. $query1->expects($this->at(0))
  487. ->method('where')
  488. ->with(['foo' => 1])
  489. ->will($this->returnSelf());
  490. $query1->expects($this->at(1))
  491. ->method('where')
  492. ->with(['article_id' => 1])
  493. ->will($this->returnSelf());
  494. $query1->expects($this->at(2))
  495. ->method('andWhere')
  496. ->with(['tag_id' => 2])
  497. ->will($this->returnSelf());
  498. $query1->expects($this->once())
  499. ->method('union')
  500. ->with($query2)
  501. ->will($this->returnSelf());
  502. $query2->expects($this->at(0))
  503. ->method('where')
  504. ->with(['foo' => 1])
  505. ->will($this->returnSelf());
  506. $query2->expects($this->at(1))
  507. ->method('where')
  508. ->with(['article_id' => 1])
  509. ->will($this->returnSelf());
  510. $query2->expects($this->at(2))
  511. ->method('andWhere')
  512. ->with(['tag_id' => 3])
  513. ->will($this->returnSelf());
  514. $jointEntities = [
  515. new Entity(['article_id' => 1, 'tag_id' => 2]),
  516. new Entity(['article_id' => 1, 'tag_id' => 3])
  517. ];
  518. $query1->expects($this->once())
  519. ->method('toArray')
  520. ->will($this->returnValue($jointEntities));
  521. $opts = ['markNew' => false];
  522. $tags = [new Entity(['id' => 2], $opts), new Entity(['id' => 3], $opts)];
  523. $entity = new Entity(['id' => 1, 'test' => $tags], $opts);
  524. $joint->expects($this->at(2))
  525. ->method('delete')
  526. ->with($jointEntities[0]);
  527. $joint->expects($this->at(3))
  528. ->method('delete')
  529. ->with($jointEntities[1]);
  530. $assoc->unlink($entity, $tags);
  531. $this->assertEmpty($entity->get('test'));
  532. }
  533. /**
  534. * Tests that unlinking with last parameter set to false
  535. * will not remove entities from the association property
  536. *
  537. * @return void
  538. */
  539. public function testUnlinkWithoutPropertyClean()
  540. {
  541. $connection = ConnectionManager::get('test');
  542. $joint = $this->getMock(
  543. '\Cake\ORM\Table',
  544. ['delete', 'find'],
  545. [['alias' => 'ArticlesTags', 'connection' => $connection]]
  546. );
  547. $config = [
  548. 'sourceTable' => $this->article,
  549. 'targetTable' => $this->tag,
  550. 'through' => $joint,
  551. 'joinTable' => 'tags_articles'
  552. ];
  553. $assoc = new BelongsToMany('Test', $config);
  554. $assoc
  555. ->junction()
  556. ->association('tags')
  557. ->conditions(['foo' => 1]);
  558. $joint->expects($this->never())->method('find');
  559. $opts = ['markNew' => false];
  560. $jointEntities = [
  561. new Entity(['article_id' => 1, 'tag_id' => 2]),
  562. new Entity(['article_id' => 1, 'tag_id' => 3])
  563. ];
  564. $tags = [
  565. new Entity(['id' => 2, '_joinData' => $jointEntities[0]], $opts),
  566. new Entity(['id' => 3, '_joinData' => $jointEntities[1]], $opts)
  567. ];
  568. $entity = new Entity(['id' => 1, 'test' => $tags], $opts);
  569. $joint->expects($this->at(0))
  570. ->method('delete')
  571. ->with($jointEntities[0]);
  572. $joint->expects($this->at(1))
  573. ->method('delete')
  574. ->with($jointEntities[1]);
  575. $assoc->unlink($entity, $tags, false);
  576. $this->assertEquals($tags, $entity->get('test'));
  577. }
  578. /**
  579. * Tests that replaceLink requires the sourceEntity to have primaryKey values
  580. * for the source entity
  581. *
  582. * @expectedException \InvalidArgumentException
  583. * @expectedExceptionMessage Could not find primary key value for source entity
  584. * @return void
  585. */
  586. public function testReplaceWithMissingPrimaryKey()
  587. {
  588. $config = [
  589. 'sourceTable' => $this->article,
  590. 'targetTable' => $this->tag,
  591. 'joinTable' => 'tags_articles'
  592. ];
  593. $assoc = new BelongsToMany('Test', $config);
  594. $entity = new Entity(['foo' => 1], ['markNew' => false]);
  595. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  596. $assoc->replaceLinks($entity, $tags);
  597. }
  598. /**
  599. * Test that replaceLinks() can saveAssociated an empty set, removing all rows.
  600. *
  601. * @return void
  602. */
  603. public function testReplaceLinksUpdateToEmptySet()
  604. {
  605. $connection = ConnectionManager::get('test');
  606. $joint = $this->getMock(
  607. '\Cake\ORM\Table',
  608. ['delete', 'find'],
  609. [['alias' => 'ArticlesTags', 'connection' => $connection]]
  610. );
  611. $config = [
  612. 'sourceTable' => $this->article,
  613. 'targetTable' => $this->tag,
  614. 'through' => $joint,
  615. 'joinTable' => 'tags_articles'
  616. ];
  617. $assoc = $this->getMock(
  618. '\Cake\ORM\Association\BelongsToMany',
  619. ['_collectJointEntities', '_saveTarget'],
  620. ['tags', $config]
  621. );
  622. $assoc->junction();
  623. $this->article
  624. ->association('ArticlesTags')
  625. ->conditions(['foo' => 1]);
  626. $query1 = $this->getMock(
  627. '\Cake\ORM\Query',
  628. ['where', 'andWhere', 'addDefaultTypes'],
  629. [$connection, $joint]
  630. );
  631. $joint->expects($this->at(0))->method('find')
  632. ->with('all')
  633. ->will($this->returnValue($query1));
  634. $query1->expects($this->at(0))
  635. ->method('where')
  636. ->with(['foo' => 1])
  637. ->will($this->returnSelf());
  638. $query1->expects($this->at(1))
  639. ->method('where')
  640. ->with(['article_id' => 1])
  641. ->will($this->returnSelf());
  642. $existing = [
  643. new Entity(['article_id' => 1, 'tag_id' => 2]),
  644. new Entity(['article_id' => 1, 'tag_id' => 4]),
  645. ];
  646. $query1->setResult(new \ArrayIterator($existing));
  647. $tags = [];
  648. $entity = new Entity(['id' => 1, 'test' => $tags]);
  649. $assoc->expects($this->once())->method('_collectJointEntities')
  650. ->with($entity, $tags)
  651. ->will($this->returnValue([]));
  652. $joint->expects($this->at(1))
  653. ->method('delete')
  654. ->with($existing[0]);
  655. $joint->expects($this->at(2))
  656. ->method('delete')
  657. ->with($existing[1]);
  658. $assoc->expects($this->never())
  659. ->method('_saveTarget');
  660. $assoc->replaceLinks($entity, $tags);
  661. $this->assertSame([], $entity->tags);
  662. $this->assertFalse($entity->dirty('tags'));
  663. }
  664. /**
  665. * Tests that replaceLinks will delete entities not present in the passed,
  666. * array, maintain those are already persisted and were passed and also
  667. * insert the rest.
  668. *
  669. * @return void
  670. */
  671. public function testReplaceLinkSuccess()
  672. {
  673. $connection = ConnectionManager::get('test');
  674. $joint = $this->getMock(
  675. '\Cake\ORM\Table',
  676. ['delete', 'find'],
  677. [['alias' => 'ArticlesTags', 'connection' => $connection]]
  678. );
  679. $config = [
  680. 'sourceTable' => $this->article,
  681. 'targetTable' => $this->tag,
  682. 'through' => $joint,
  683. 'joinTable' => 'tags_articles'
  684. ];
  685. $assoc = $this->getMock(
  686. '\Cake\ORM\Association\BelongsToMany',
  687. ['_collectJointEntities', '_saveTarget'],
  688. ['tags', $config]
  689. );
  690. $assoc->junction();
  691. $this->article
  692. ->association('ArticlesTags')
  693. ->conditions(['foo' => 1]);
  694. $query1 = $this->getMock(
  695. '\Cake\ORM\Query',
  696. ['where', 'andWhere', 'addDefaultTypes'],
  697. [$connection, $joint]
  698. );
  699. $joint->expects($this->at(0))->method('find')
  700. ->with('all')
  701. ->will($this->returnValue($query1));
  702. $query1->expects($this->at(0))
  703. ->method('where')
  704. ->with(['foo' => 1])
  705. ->will($this->returnSelf());
  706. $query1->expects($this->at(1))
  707. ->method('where')
  708. ->with(['article_id' => 1])
  709. ->will($this->returnSelf());
  710. $existing = [
  711. new Entity(['article_id' => 1, 'tag_id' => 2]),
  712. new Entity(['article_id' => 1, 'tag_id' => 4]),
  713. new Entity(['article_id' => 1, 'tag_id' => 5]),
  714. new Entity(['article_id' => 1, 'tag_id' => 6])
  715. ];
  716. $query1->setResult(new \ArrayIterator($existing));
  717. $opts = ['markNew' => false];
  718. $tags = [
  719. new Entity(['id' => 2], $opts),
  720. new Entity(['id' => 3], $opts),
  721. new Entity(['id' => 6])
  722. ];
  723. $entity = new Entity(['id' => 1, 'test' => $tags], $opts);
  724. $jointEntities = [
  725. new Entity(['article_id' => 1, 'tag_id' => 2])
  726. ];
  727. $assoc->expects($this->once())->method('_collectJointEntities')
  728. ->with($entity, $tags)
  729. ->will($this->returnValue($jointEntities));
  730. $joint->expects($this->at(1))
  731. ->method('delete')
  732. ->with($existing[1]);
  733. $joint->expects($this->at(2))
  734. ->method('delete')
  735. ->with($existing[2]);
  736. $options = ['foo' => 'bar'];
  737. $assoc->expects($this->once())
  738. ->method('_saveTarget')
  739. ->with($entity, [1 => $tags[1], 2 => $tags[2]], $options + ['associated' => false])
  740. ->will($this->returnCallback(function ($entity, $inserts) use ($tags) {
  741. $this->assertSame([1 => $tags[1], 2 => $tags[2]], $inserts);
  742. $entity->tags = $inserts;
  743. return true;
  744. }));
  745. $assoc->replaceLinks($entity, $tags, $options + ['associated' => false]);
  746. $this->assertSame($tags, $entity->tags);
  747. $this->assertFalse($entity->dirty('tags'));
  748. }
  749. /**
  750. * Test that saving an empty set on create works.
  751. *
  752. * @return void
  753. */
  754. public function testSaveAssociatedEmptySetSuccess()
  755. {
  756. $assoc = $this->getMock(
  757. '\Cake\ORM\Association\BelongsToMany',
  758. ['_saveTarget', 'replaceLinks'],
  759. ['tags']
  760. );
  761. $entity = new Entity([
  762. 'id' => 1,
  763. 'tags' => []
  764. ], ['markNew' => true]);
  765. $assoc->saveStrategy(BelongsToMany::SAVE_REPLACE);
  766. $assoc->expects($this->never())
  767. ->method('replaceLinks');
  768. $assoc->expects($this->never())
  769. ->method('_saveTarget');
  770. $this->assertSame($entity, $assoc->saveAssociated($entity));
  771. }
  772. /**
  773. * Tests saving with replace strategy returning true
  774. *
  775. * @return void
  776. */
  777. public function testSaveAssociatedWithReplace()
  778. {
  779. $assoc = $this->getMock(
  780. '\Cake\ORM\Association\BelongsToMany',
  781. ['replaceLinks'],
  782. ['tags']
  783. );
  784. $entity = new Entity([
  785. 'id' => 1,
  786. 'tags' => [
  787. new Entity(['name' => 'foo'])
  788. ]
  789. ]);
  790. $options = ['foo' => 'bar'];
  791. $assoc->saveStrategy(BelongsToMany::SAVE_REPLACE);
  792. $assoc->expects($this->once())->method('replaceLinks')
  793. ->with($entity, $entity->tags, $options)
  794. ->will($this->returnValue(true));
  795. $this->assertSame($entity, $assoc->saveAssociated($entity, $options));
  796. }
  797. /**
  798. * Tests saving with replace strategy returning true
  799. *
  800. * @return void
  801. */
  802. public function testSaveAssociatedWithReplaceReturnFalse()
  803. {
  804. $assoc = $this->getMock(
  805. '\Cake\ORM\Association\BelongsToMany',
  806. ['replaceLinks'],
  807. ['tags']
  808. );
  809. $entity = new Entity([
  810. 'id' => 1,
  811. 'tags' => [
  812. new Entity(['name' => 'foo'])
  813. ]
  814. ]);
  815. $options = ['foo' => 'bar'];
  816. $assoc->saveStrategy(BelongsToMany::SAVE_REPLACE);
  817. $assoc->expects($this->once())->method('replaceLinks')
  818. ->with($entity, $entity->tags, $options)
  819. ->will($this->returnValue(false));
  820. $this->assertFalse($assoc->saveAssociated($entity, $options));
  821. }
  822. /**
  823. * Test that saveAssociated() ignores non entity values.
  824. *
  825. * @return void
  826. */
  827. public function testSaveAssociatedOnlyEntities()
  828. {
  829. $connection = ConnectionManager::get('test');
  830. $mock = $this->getMock(
  831. 'Cake\ORM\Table',
  832. ['saveAssociated', 'schema'],
  833. [['table' => 'tags', 'connection' => $connection]]
  834. );
  835. $mock->primaryKey('id');
  836. $config = [
  837. 'sourceTable' => $this->article,
  838. 'targetTable' => $mock,
  839. 'saveStrategy' => BelongsToMany::SAVE_APPEND,
  840. ];
  841. $entity = new Entity([
  842. 'id' => 1,
  843. 'title' => 'First Post',
  844. 'tags' => [
  845. ['tag' => 'nope'],
  846. new Entity(['tag' => 'cakephp']),
  847. ]
  848. ]);
  849. $mock->expects($this->never())
  850. ->method('saveAssociated');
  851. $association = new BelongsToMany('Tags', $config);
  852. $association->saveAssociated($entity);
  853. }
  854. /**
  855. * Tests that targetForeignKey() returns the correct configured value
  856. *
  857. * @return void
  858. */
  859. public function testTargetForeignKey()
  860. {
  861. $assoc = new BelongsToMany('Test', [
  862. 'sourceTable' => $this->article,
  863. 'targetTable' => $this->tag
  864. ]);
  865. $this->assertEquals('tag_id', $assoc->targetForeignKey());
  866. $assoc->targetForeignKey('another_key');
  867. $this->assertEquals('another_key', $assoc->targetForeignKey());
  868. $assoc = new BelongsToMany('Test', [
  869. 'sourceTable' => $this->article,
  870. 'targetTable' => $this->tag,
  871. 'targetForeignKey' => 'foo'
  872. ]);
  873. $this->assertEquals('foo', $assoc->targetForeignKey());
  874. }
  875. /**
  876. * Tests that custom foreignKeys are properly trasmitted to involved associations
  877. * when they are customized
  878. *
  879. * @return void
  880. */
  881. public function testJunctionWithCustomForeignKeys()
  882. {
  883. $assoc = new BelongsToMany('Test', [
  884. 'sourceTable' => $this->article,
  885. 'targetTable' => $this->tag,
  886. 'foreignKey' => 'Art',
  887. 'targetForeignKey' => 'Tag'
  888. ]);
  889. $junction = $assoc->junction();
  890. $this->assertEquals('Art', $junction->association('Articles')->foreignKey());
  891. $this->assertEquals('Tag', $junction->association('Tags')->foreignKey());
  892. $inverseRelation = $this->tag->association('Articles');
  893. $this->assertEquals('Tag', $inverseRelation->foreignKey());
  894. $this->assertEquals('Art', $inverseRelation->targetForeignKey());
  895. }
  896. /**
  897. * Tests that property is being set using the constructor options.
  898. *
  899. * @return void
  900. */
  901. public function testPropertyOption()
  902. {
  903. $config = ['propertyName' => 'thing_placeholder'];
  904. $association = new BelongsToMany('Thing', $config);
  905. $this->assertEquals('thing_placeholder', $association->property());
  906. }
  907. /**
  908. * Test that plugin names are omitted from property()
  909. *
  910. * @return void
  911. */
  912. public function testPropertyNoPlugin()
  913. {
  914. $mock = $this->getMock('Cake\ORM\Table', [], [], '', false);
  915. $config = [
  916. 'sourceTable' => $this->article,
  917. 'targetTable' => $mock,
  918. ];
  919. $association = new BelongsToMany('Contacts.Tags', $config);
  920. $this->assertEquals('tags', $association->property());
  921. }
  922. }