BelongsToManyTest.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503
  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\QueryExpression;
  17. use Cake\Datasource\ConnectionManager;
  18. use Cake\Event\Event;
  19. use Cake\ORM\Association\BelongsTo;
  20. use Cake\ORM\Association\BelongsToMany;
  21. use Cake\ORM\Association\HasMany;
  22. use Cake\ORM\Entity;
  23. use Cake\ORM\Table;
  24. use Cake\TestSuite\TestCase;
  25. /**
  26. * Tests BelongsToMany class
  27. */
  28. class BelongsToManyTest extends TestCase
  29. {
  30. /**
  31. * Fixtures
  32. *
  33. * @var array
  34. */
  35. public $fixtures = ['core.Articles', 'core.SpecialTags', 'core.ArticlesTags', 'core.Tags'];
  36. /**
  37. * Set up
  38. *
  39. * @return void
  40. */
  41. public function setUp()
  42. {
  43. parent::setUp();
  44. $this->tag = $this->getMockBuilder('Cake\ORM\Table')
  45. ->setMethods(['find', 'delete'])
  46. ->setConstructorArgs([['alias' => 'Tags', 'table' => 'tags']])
  47. ->getMock();
  48. $this->tag->setSchema([
  49. 'id' => ['type' => 'integer'],
  50. 'name' => ['type' => 'string'],
  51. '_constraints' => [
  52. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  53. ],
  54. ]);
  55. $this->article = $this->getMockBuilder('Cake\ORM\Table')
  56. ->setMethods(['find', 'delete'])
  57. ->setConstructorArgs([['alias' => 'Articles', 'table' => 'articles']])
  58. ->getMock();
  59. $this->article->setSchema([
  60. 'id' => ['type' => 'integer'],
  61. 'name' => ['type' => 'string'],
  62. '_constraints' => [
  63. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  64. ],
  65. ]);
  66. }
  67. /**
  68. * Tests setForeignKey()
  69. *
  70. * @return void
  71. */
  72. public function testSetForeignKey()
  73. {
  74. $assoc = new BelongsToMany('Test', [
  75. 'sourceTable' => $this->article,
  76. 'targetTable' => $this->tag,
  77. ]);
  78. $this->assertEquals('article_id', $assoc->getForeignKey());
  79. $this->assertSame($assoc, $assoc->setForeignKey('another_key'));
  80. $this->assertEquals('another_key', $assoc->getForeignKey());
  81. }
  82. /**
  83. * Tests that foreignKey() returns the correct configured value
  84. *
  85. * @group deprecated
  86. * @return void
  87. */
  88. public function testForeignKey()
  89. {
  90. $this->deprecated(function () {
  91. $assoc = new BelongsToMany('Test', [
  92. 'sourceTable' => $this->article,
  93. 'targetTable' => $this->tag,
  94. ]);
  95. $this->assertEquals('article_id', $assoc->foreignKey());
  96. $this->assertEquals('another_key', $assoc->foreignKey('another_key'));
  97. $this->assertEquals('another_key', $assoc->foreignKey());
  98. });
  99. }
  100. /**
  101. * Tests that the association reports it can be joined
  102. *
  103. * @return void
  104. */
  105. public function testCanBeJoined()
  106. {
  107. $assoc = new BelongsToMany('Test');
  108. $this->assertFalse($assoc->canBeJoined());
  109. }
  110. /**
  111. * Tests sort() method
  112. *
  113. * @group deprecated
  114. * @return void
  115. */
  116. public function testSort()
  117. {
  118. $this->deprecated(function () {
  119. $assoc = new BelongsToMany('Test');
  120. $this->assertNull($assoc->sort());
  121. $assoc->sort(['id' => 'ASC']);
  122. $this->assertEquals(['id' => 'ASC'], $assoc->sort());
  123. });
  124. }
  125. /**
  126. * Tests setSort() method
  127. *
  128. * @return void
  129. */
  130. public function testSetSort()
  131. {
  132. $assoc = new BelongsToMany('Test');
  133. $this->assertNull($assoc->getSort());
  134. $assoc->setSort(['id' => 'ASC']);
  135. $this->assertEquals(['id' => 'ASC'], $assoc->getSort());
  136. }
  137. /**
  138. * Tests requiresKeys() method
  139. *
  140. * @return void
  141. */
  142. public function testRequiresKeys()
  143. {
  144. $assoc = new BelongsToMany('Test');
  145. $this->assertTrue($assoc->requiresKeys());
  146. $assoc->setStrategy(BelongsToMany::STRATEGY_SUBQUERY);
  147. $this->assertFalse($assoc->requiresKeys());
  148. $assoc->setStrategy(BelongsToMany::STRATEGY_SELECT);
  149. $this->assertTrue($assoc->requiresKeys());
  150. }
  151. /**
  152. * Tests that BelongsToMany can't use the join strategy
  153. *
  154. * @return void
  155. */
  156. public function testStrategyFailure()
  157. {
  158. $this->expectException(\InvalidArgumentException::class);
  159. $this->expectExceptionMessage('Invalid strategy "join" was provided');
  160. $assoc = new BelongsToMany('Test');
  161. $assoc->setStrategy(BelongsToMany::STRATEGY_JOIN);
  162. }
  163. /**
  164. * Tests the junction method
  165. *
  166. * @return void
  167. */
  168. public function testJunction()
  169. {
  170. $assoc = new BelongsToMany('Test', [
  171. 'sourceTable' => $this->article,
  172. 'targetTable' => $this->tag,
  173. 'strategy' => 'subquery',
  174. ]);
  175. $junction = $assoc->junction();
  176. $this->assertInstanceOf(Table::class, $junction);
  177. $this->assertEquals('ArticlesTags', $junction->getAlias());
  178. $this->assertEquals('articles_tags', $junction->getTable());
  179. $this->assertSame($this->article, $junction->getAssociation('Articles')->getTarget());
  180. $this->assertSame($this->tag, $junction->getAssociation('Tags')->getTarget());
  181. $this->assertInstanceOf(BelongsTo::class, $junction->getAssociation('Articles'));
  182. $this->assertInstanceOf(BelongsTo::class, $junction->getAssociation('Tags'));
  183. $this->assertSame($junction, $this->tag->getAssociation('ArticlesTags')->getTarget());
  184. $this->assertSame($this->article, $this->tag->getAssociation('Articles')->getTarget());
  185. $this->assertInstanceOf(BelongsToMany::class, $this->tag->getAssociation('Articles'));
  186. $this->assertInstanceOf(HasMany::class, $this->tag->getAssociation('ArticlesTags'));
  187. $this->assertSame($junction, $assoc->junction());
  188. $junction2 = $this->getTableLocator()->get('Foos');
  189. $assoc->junction($junction2);
  190. $this->assertSame($junction2, $assoc->junction());
  191. $assoc->junction('ArticlesTags');
  192. $this->assertSame($junction, $assoc->junction());
  193. $this->assertSame($assoc->getStrategy(), $this->tag->getAssociation('Articles')->getStrategy());
  194. $this->assertSame($assoc->getStrategy(), $this->tag->getAssociation('ArticlesTags')->getStrategy());
  195. $this->assertSame($assoc->getStrategy(), $this->article->getAssociation('ArticlesTags')->getStrategy());
  196. $this->assertSame($this->article->getPrimaryKey(), $junction->getAssociation('Articles')->getBindingKey());
  197. $this->assertSame($this->tag->getPrimaryKey(), $junction->getAssociation('Tags')->getBindingKey());
  198. }
  199. /**
  200. * Tests the junction passes the source connection name on.
  201. *
  202. * @return void
  203. */
  204. public function testJunctionConnection()
  205. {
  206. $mock = $this->getMockBuilder('Cake\Database\Connection')
  207. ->setMethods(['setDriver'])
  208. ->setConstructorArgs(['name' => 'other_source'])
  209. ->getMock();
  210. ConnectionManager::setConfig('other_source', $mock);
  211. $this->article->setConnection(ConnectionManager::get('other_source'));
  212. $assoc = new BelongsToMany('Test', [
  213. 'sourceTable' => $this->article,
  214. 'targetTable' => $this->tag,
  215. ]);
  216. $junction = $assoc->junction();
  217. $this->assertSame($mock, $junction->getConnection());
  218. ConnectionManager::drop('other_source');
  219. }
  220. /**
  221. * Tests the junction method custom keys
  222. *
  223. * @return void
  224. */
  225. public function testJunctionCustomKeys()
  226. {
  227. $this->article->belongsToMany('Tags', [
  228. 'joinTable' => 'articles_tags',
  229. 'foreignKey' => 'article',
  230. 'targetForeignKey' => 'tag',
  231. ]);
  232. $this->tag->belongsToMany('Articles', [
  233. 'joinTable' => 'articles_tags',
  234. 'foreignKey' => 'tag',
  235. 'targetForeignKey' => 'article',
  236. ]);
  237. $junction = $this->article->getAssociation('Tags')->junction();
  238. $this->assertEquals('article', $junction->getAssociation('Articles')->getForeignKey());
  239. $this->assertEquals('article', $this->article->getAssociation('ArticlesTags')->getForeignKey());
  240. $junction = $this->tag->getAssociation('Articles')->junction();
  241. $this->assertEquals('tag', $junction->getAssociation('Tags')->getForeignKey());
  242. $this->assertEquals('tag', $this->tag->getAssociation('ArticlesTags')->getForeignKey());
  243. }
  244. /**
  245. * Tests it is possible to set the table name for the join table
  246. *
  247. * @return void
  248. */
  249. public function testJunctionWithDefaultTableName()
  250. {
  251. $assoc = new BelongsToMany('Test', [
  252. 'sourceTable' => $this->article,
  253. 'targetTable' => $this->tag,
  254. 'joinTable' => 'tags_articles',
  255. ]);
  256. $junction = $assoc->junction();
  257. $this->assertEquals('TagsArticles', $junction->getAlias());
  258. $this->assertEquals('tags_articles', $junction->getTable());
  259. }
  260. /**
  261. * Tests saveStrategy
  262. *
  263. * @group deprecated
  264. * @return void
  265. */
  266. public function testSaveStrategy()
  267. {
  268. $this->deprecated(function () {
  269. $assoc = new BelongsToMany('Test');
  270. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->saveStrategy());
  271. $assoc->saveStrategy(BelongsToMany::SAVE_APPEND);
  272. $this->assertEquals(BelongsToMany::SAVE_APPEND, $assoc->saveStrategy());
  273. $assoc->saveStrategy(BelongsToMany::SAVE_REPLACE);
  274. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->saveStrategy());
  275. });
  276. }
  277. /**
  278. * Tests saveStrategy
  279. *
  280. * @return void
  281. */
  282. public function testSetSaveStrategy()
  283. {
  284. $assoc = new BelongsToMany('Test');
  285. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->getSaveStrategy());
  286. $assoc->setSaveStrategy(BelongsToMany::SAVE_APPEND);
  287. $this->assertEquals(BelongsToMany::SAVE_APPEND, $assoc->getSaveStrategy());
  288. $assoc->setSaveStrategy(BelongsToMany::SAVE_REPLACE);
  289. $this->assertEquals(BelongsToMany::SAVE_REPLACE, $assoc->getSaveStrategy());
  290. }
  291. /**
  292. * Tests that it is possible to pass the saveAssociated strategy in the constructor
  293. *
  294. * @return void
  295. */
  296. public function testSaveStrategyInOptions()
  297. {
  298. $assoc = new BelongsToMany('Test', ['saveStrategy' => BelongsToMany::SAVE_APPEND]);
  299. $this->assertEquals(BelongsToMany::SAVE_APPEND, $assoc->getSaveStrategy());
  300. }
  301. /**
  302. * Tests that passing an invalid strategy will throw an exception
  303. *
  304. * @return void
  305. */
  306. public function testSaveStrategyInvalid()
  307. {
  308. $this->expectException(\InvalidArgumentException::class);
  309. $this->expectExceptionMessage('Invalid save strategy "depsert"');
  310. $assoc = new BelongsToMany('Test', ['saveStrategy' => 'depsert']);
  311. }
  312. /**
  313. * Test cascading deletes.
  314. *
  315. * @return void
  316. */
  317. public function testCascadeDelete()
  318. {
  319. $articleTag = $this->getMockBuilder('Cake\ORM\Table')
  320. ->setMethods(['deleteAll'])
  321. ->getMock();
  322. $config = [
  323. 'sourceTable' => $this->article,
  324. 'targetTable' => $this->tag,
  325. 'sort' => ['id' => 'ASC'],
  326. ];
  327. $association = new BelongsToMany('Tags', $config);
  328. $association->junction($articleTag);
  329. $this->article
  330. ->getAssociation($articleTag->getAlias())
  331. ->setConditions(['click_count' => 3]);
  332. $articleTag->expects($this->once())
  333. ->method('deleteAll')
  334. ->with([
  335. 'click_count' => 3,
  336. 'article_id' => 1,
  337. ]);
  338. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  339. $association->cascadeDelete($entity);
  340. }
  341. /**
  342. * Test cascading deletes with dependent=false
  343. *
  344. * @return void
  345. */
  346. public function testCascadeDeleteDependent()
  347. {
  348. $articleTag = $this->getMockBuilder('Cake\ORM\Table')
  349. ->setMethods(['delete', 'deleteAll'])
  350. ->getMock();
  351. $config = [
  352. 'sourceTable' => $this->article,
  353. 'targetTable' => $this->tag,
  354. 'dependent' => false,
  355. 'sort' => ['id' => 'ASC'],
  356. ];
  357. $association = new BelongsToMany('Tags', $config);
  358. $association->junction($articleTag);
  359. $this->article
  360. ->getAssociation($articleTag->getAlias())
  361. ->setConditions(['click_count' => 3]);
  362. $articleTag->expects($this->never())
  363. ->method('deleteAll');
  364. $articleTag->expects($this->never())
  365. ->method('delete');
  366. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  367. $association->cascadeDelete($entity);
  368. }
  369. /**
  370. * Test cascading deletes with callbacks.
  371. *
  372. * @return void
  373. */
  374. public function testCascadeDeleteWithCallbacks()
  375. {
  376. $articleTag = $this->getTableLocator()->get('ArticlesTags');
  377. $config = [
  378. 'sourceTable' => $this->article,
  379. 'targetTable' => $this->tag,
  380. 'cascadeCallbacks' => true,
  381. ];
  382. $association = new BelongsToMany('Tag', $config);
  383. $association->junction($articleTag);
  384. $this->article->getAssociation($articleTag->getAlias());
  385. $counter = $this->getMockBuilder('StdClass')
  386. ->setMethods(['__invoke'])
  387. ->getMock();
  388. $counter->expects($this->exactly(2))->method('__invoke');
  389. $articleTag->getEventManager()->on('Model.beforeDelete', $counter);
  390. $this->assertEquals(2, $articleTag->find()->where(['article_id' => 1])->count());
  391. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  392. $association->cascadeDelete($entity);
  393. $this->assertEquals(0, $articleTag->find()->where(['article_id' => 1])->count());
  394. }
  395. /**
  396. * Test linking entities having a non persisted source entity
  397. *
  398. * @return void
  399. */
  400. public function testLinkWithNotPersistedSource()
  401. {
  402. $this->expectException(\InvalidArgumentException::class);
  403. $this->expectExceptionMessage('Source entity needs to be persisted before links can be created or removed');
  404. $config = [
  405. 'sourceTable' => $this->article,
  406. 'targetTable' => $this->tag,
  407. 'joinTable' => 'tags_articles',
  408. ];
  409. $assoc = new BelongsToMany('Test', $config);
  410. $entity = new Entity(['id' => 1]);
  411. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  412. $assoc->link($entity, $tags);
  413. }
  414. /**
  415. * Test liking entities having a non persisted target entity
  416. *
  417. * @return void
  418. */
  419. public function testLinkWithNotPersistedTarget()
  420. {
  421. $this->expectException(\InvalidArgumentException::class);
  422. $this->expectExceptionMessage('Cannot link entities that have not been persisted yet');
  423. $config = [
  424. 'sourceTable' => $this->article,
  425. 'targetTable' => $this->tag,
  426. 'joinTable' => 'tags_articles',
  427. ];
  428. $assoc = new BelongsToMany('Test', $config);
  429. $entity = new Entity(['id' => 1], ['markNew' => false]);
  430. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  431. $assoc->link($entity, $tags);
  432. }
  433. /**
  434. * Tests that linking entities will persist correctly with append strategy
  435. *
  436. * @return void
  437. */
  438. public function testLinkSuccessSaveAppend()
  439. {
  440. $articles = $this->getTableLocator()->get('Articles');
  441. $tags = $this->getTableLocator()->get('Tags');
  442. $config = [
  443. 'sourceTable' => $articles,
  444. 'targetTable' => $tags,
  445. 'joinTable' => 'articles_tags',
  446. 'saveStrategy' => BelongsToMany::SAVE_APPEND,
  447. ];
  448. $assoc = $articles->belongsToMany('Tags', $config);
  449. // Load without tags as that is a main use case for append strategies
  450. $article = $articles->get(1);
  451. $opts = ['markNew' => false];
  452. $tags = [
  453. new Entity(['id' => 2, 'name' => 'add'], $opts),
  454. new Entity(['id' => 3, 'name' => 'adder'], $opts),
  455. ];
  456. $this->assertTrue($assoc->link($article, $tags));
  457. $this->assertCount(2, $article->tags, 'In-memory tags are incorrect');
  458. $this->assertSame([2, 3], collection($article->tags)->extract('id')->toList());
  459. $article = $articles->get(1, ['contain' => ['Tags']]);
  460. $this->assertCount(3, $article->tags, 'Persisted tags are wrong');
  461. $this->assertSame([1, 2, 3], collection($article->tags)->extract('id')->toList());
  462. }
  463. /**
  464. * Tests that linking the same tag to multiple articles works
  465. *
  466. * @return void
  467. */
  468. public function testLinkSaveAppendSharedTarget()
  469. {
  470. $articles = $this->getTableLocator()->get('Articles');
  471. $tags = $this->getTableLocator()->get('Tags');
  472. $articlesTags = $this->getTableLocator()->get('ArticlesTags');
  473. $articlesTags->deleteAll('1=1');
  474. $config = [
  475. 'sourceTable' => $articles,
  476. 'targetTable' => $tags,
  477. 'joinTable' => 'articles_tags',
  478. 'saveStrategy' => BelongsToMany::SAVE_APPEND,
  479. ];
  480. $assoc = $articles->belongsToMany('Tags', $config);
  481. $articleOne = $articles->get(1);
  482. $articleTwo = $articles->get(2);
  483. $tagTwo = $tags->get(2);
  484. $tagThree = $tags->get(3);
  485. $this->assertTrue($assoc->link($articleOne, [$tagThree, $tagTwo]));
  486. $this->assertTrue($assoc->link($articleTwo, [$tagThree]));
  487. $this->assertCount(2, $articleOne->tags, 'In-memory tags are incorrect');
  488. $this->assertSame([3, 2], collection($articleOne->tags)->extract('id')->toList());
  489. $this->assertCount(1, $articleTwo->tags, 'In-memory tags are incorrect');
  490. $this->assertSame([3], collection($articleTwo->tags)->extract('id')->toList());
  491. $rows = $articlesTags->find()->all();
  492. $this->assertCount(3, $rows, '3 link rows should be created.');
  493. }
  494. /**
  495. * Tests that liking entities will validate data and pass on to _saveLinks
  496. *
  497. * @return void
  498. */
  499. public function testLinkSuccessWithMocks()
  500. {
  501. $connection = ConnectionManager::get('test');
  502. $joint = $this->getMockBuilder('\Cake\ORM\Table')
  503. ->setMethods(['save', 'getPrimaryKey'])
  504. ->setConstructorArgs([['alias' => 'ArticlesTags', 'connection' => $connection]])
  505. ->getMock();
  506. $config = [
  507. 'sourceTable' => $this->article,
  508. 'targetTable' => $this->tag,
  509. 'through' => $joint,
  510. 'joinTable' => 'tags_articles',
  511. ];
  512. $assoc = new BelongsToMany('Test', $config);
  513. $opts = ['markNew' => false];
  514. $entity = new Entity(['id' => 1], $opts);
  515. $tags = [new Entity(['id' => 2], $opts), new Entity(['id' => 3], $opts)];
  516. $saveOptions = ['foo' => 'bar'];
  517. $joint->method('getPrimaryKey')
  518. ->will($this->returnValue(['article_id', 'tag_id']));
  519. $joint->expects($this->at(1))
  520. ->method('save')
  521. ->will($this->returnCallback(function ($e, $opts) use ($entity) {
  522. $expected = ['article_id' => 1, 'tag_id' => 2];
  523. $this->assertEquals($expected, $e->toArray());
  524. $this->assertEquals(['foo' => 'bar'], $opts);
  525. $this->assertTrue($e->isNew());
  526. return $entity;
  527. }));
  528. $joint->expects($this->at(2))
  529. ->method('save')
  530. ->will($this->returnCallback(function ($e, $opts) use ($entity) {
  531. $expected = ['article_id' => 1, 'tag_id' => 3];
  532. $this->assertEquals($expected, $e->toArray());
  533. $this->assertEquals(['foo' => 'bar'], $opts);
  534. $this->assertTrue($e->isNew());
  535. return $entity;
  536. }));
  537. $this->assertTrue($assoc->link($entity, $tags, $saveOptions));
  538. $this->assertSame($entity->test, $tags);
  539. }
  540. /**
  541. * Tests that linking entities will set the junction table registry alias
  542. *
  543. * @return void
  544. */
  545. public function testLinkSetSourceToJunctionEntities()
  546. {
  547. $connection = ConnectionManager::get('test');
  548. $joint = $this->getMockBuilder('\Cake\ORM\Table')
  549. ->setMethods(['save', 'getPrimaryKey'])
  550. ->setConstructorArgs([['alias' => 'ArticlesTags', 'connection' => $connection]])
  551. ->getMock();
  552. $joint->setRegistryAlias('Plugin.ArticlesTags');
  553. $config = [
  554. 'sourceTable' => $this->article,
  555. 'targetTable' => $this->tag,
  556. 'through' => $joint,
  557. ];
  558. $assoc = new BelongsToMany('Tags', $config);
  559. $opts = ['markNew' => false];
  560. $entity = new Entity(['id' => 1], $opts);
  561. $tags = [new Entity(['id' => 2], $opts)];
  562. $joint->method('getPrimaryKey')
  563. ->will($this->returnValue(['article_id', 'tag_id']));
  564. $joint->expects($this->once())
  565. ->method('save')
  566. ->will($this->returnCallback(function (Entity $e, $opts) {
  567. $this->assertSame('Plugin.ArticlesTags', $e->getSource());
  568. return $e;
  569. }));
  570. $this->assertTrue($assoc->link($entity, $tags));
  571. $this->assertSame($entity->tags, $tags);
  572. $this->assertSame('Plugin.ArticlesTags', $entity->tags[0]->get('_joinData')->getSource());
  573. }
  574. /**
  575. * Test liking entities having a non persisted source entity
  576. *
  577. * @return void
  578. */
  579. public function testUnlinkWithNotPersistedSource()
  580. {
  581. $this->expectException(\InvalidArgumentException::class);
  582. $this->expectExceptionMessage('Source entity needs to be persisted before links can be created or removed');
  583. $config = [
  584. 'sourceTable' => $this->article,
  585. 'targetTable' => $this->tag,
  586. 'joinTable' => 'tags_articles',
  587. ];
  588. $assoc = new BelongsToMany('Test', $config);
  589. $entity = new Entity(['id' => 1]);
  590. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  591. $assoc->unlink($entity, $tags);
  592. }
  593. /**
  594. * Test liking entities having a non persisted target entity
  595. *
  596. * @return void
  597. */
  598. public function testUnlinkWithNotPersistedTarget()
  599. {
  600. $this->expectException(\InvalidArgumentException::class);
  601. $this->expectExceptionMessage('Cannot link entities that have not been persisted');
  602. $config = [
  603. 'sourceTable' => $this->article,
  604. 'targetTable' => $this->tag,
  605. 'joinTable' => 'tags_articles',
  606. ];
  607. $assoc = new BelongsToMany('Test', $config);
  608. $entity = new Entity(['id' => 1], ['markNew' => false]);
  609. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  610. $assoc->unlink($entity, $tags);
  611. }
  612. /**
  613. * Tests that unlinking calls the right methods
  614. *
  615. * @return void
  616. */
  617. public function testUnlinkSuccess()
  618. {
  619. $joint = $this->getTableLocator()->get('SpecialTags');
  620. $articles = $this->getTableLocator()->get('Articles');
  621. $tags = $this->getTableLocator()->get('Tags');
  622. $assoc = $articles->belongsToMany('Tags', [
  623. 'sourceTable' => $articles,
  624. 'targetTable' => $tags,
  625. 'through' => $joint,
  626. 'joinTable' => 'special_tags',
  627. ]);
  628. $entity = $articles->get(2, ['contain' => 'Tags']);
  629. $initial = $entity->tags;
  630. $this->assertCount(1, $initial);
  631. $this->assertTrue($assoc->unlink($entity, $entity->tags));
  632. $this->assertEmpty($entity->get('tags'), 'Property should be empty');
  633. $new = $articles->get(2, ['contain' => 'Tags']);
  634. $this->assertCount(0, $new->tags, 'DB should be clean');
  635. $this->assertSame(3, $tags->find()->count(), 'Tags should still exist');
  636. }
  637. /**
  638. * Tests that unlinking with last parameter set to false
  639. * will not remove entities from the association property
  640. *
  641. * @return void
  642. */
  643. public function testUnlinkWithoutPropertyClean()
  644. {
  645. $joint = $this->getTableLocator()->get('SpecialTags');
  646. $articles = $this->getTableLocator()->get('Articles');
  647. $tags = $this->getTableLocator()->get('Tags');
  648. $assoc = $articles->belongsToMany('Tags', [
  649. 'sourceTable' => $articles,
  650. 'targetTable' => $tags,
  651. 'through' => $joint,
  652. 'joinTable' => 'special_tags',
  653. 'conditions' => ['SpecialTags.highlighted' => true],
  654. ]);
  655. $entity = $articles->get(2, ['contain' => 'Tags']);
  656. $initial = $entity->tags;
  657. $this->assertCount(1, $initial);
  658. $this->assertTrue($assoc->unlink($entity, $initial, ['cleanProperty' => false]));
  659. $this->assertNotEmpty($entity->get('tags'), 'Property should not be empty');
  660. $this->assertEquals($initial, $entity->get('tags'), 'Property should be untouched');
  661. $new = $articles->get(2, ['contain' => 'Tags']);
  662. $this->assertCount(0, $new->tags, 'DB should be clean');
  663. }
  664. /**
  665. * Tests that replaceLink requires the sourceEntity to have primaryKey values
  666. * for the source entity
  667. *
  668. * @return void
  669. */
  670. public function testReplaceWithMissingPrimaryKey()
  671. {
  672. $this->expectException(\InvalidArgumentException::class);
  673. $this->expectExceptionMessage('Could not find primary key value for source entity');
  674. $config = [
  675. 'sourceTable' => $this->article,
  676. 'targetTable' => $this->tag,
  677. 'joinTable' => 'tags_articles',
  678. ];
  679. $assoc = new BelongsToMany('Test', $config);
  680. $entity = new Entity(['foo' => 1], ['markNew' => false]);
  681. $tags = [new Entity(['id' => 2]), new Entity(['id' => 3])];
  682. $assoc->replaceLinks($entity, $tags);
  683. }
  684. /**
  685. * Test that replaceLinks() can saveAssociated an empty set, removing all rows.
  686. *
  687. * @return void
  688. */
  689. public function testReplaceLinksUpdateToEmptySet()
  690. {
  691. $joint = $this->getTableLocator()->get('ArticlesTags');
  692. $articles = $this->getTableLocator()->get('Articles');
  693. $tags = $this->getTableLocator()->get('Tags');
  694. $assoc = $articles->belongsToMany('Tags', [
  695. 'sourceTable' => $articles,
  696. 'targetTable' => $tags,
  697. 'through' => $joint,
  698. 'joinTable' => 'articles_tags',
  699. ]);
  700. $entity = $articles->get(1, ['contain' => 'Tags']);
  701. $this->assertCount(2, $entity->tags);
  702. $assoc->replaceLinks($entity, []);
  703. $this->assertSame([], $entity->tags, 'Property should be empty');
  704. $this->assertFalse($entity->isDirty('tags'), 'Property should be cleaned');
  705. $new = $articles->get(1, ['contain' => 'Tags']);
  706. $this->assertSame([], $entity->tags, 'Should not be data in db');
  707. }
  708. /**
  709. * Tests that replaceLinks will delete entities not present in the passed,
  710. * array, maintain those are already persisted and were passed and also
  711. * insert the rest.
  712. *
  713. * @return void
  714. */
  715. public function testReplaceLinkSuccess()
  716. {
  717. $joint = $this->getTableLocator()->get('ArticlesTags');
  718. $articles = $this->getTableLocator()->get('Articles');
  719. $tags = $this->getTableLocator()->get('Tags');
  720. $assoc = $articles->belongsToMany('Tags', [
  721. 'sourceTable' => $articles,
  722. 'targetTable' => $tags,
  723. 'through' => $joint,
  724. 'joinTable' => 'articles_tags',
  725. ]);
  726. $entity = $articles->get(1, ['contain' => 'Tags']);
  727. // 1=existing, 2=removed, 3=new link, & new tag
  728. $tagData = [
  729. new Entity(['id' => 1], ['markNew' => false]),
  730. new Entity(['id' => 3]),
  731. new Entity(['name' => 'net new']),
  732. ];
  733. $result = $assoc->replaceLinks($entity, $tagData, ['associated' => false]);
  734. $this->assertTrue($result);
  735. $this->assertSame($tagData, $entity->tags, 'Tags should match replaced objects');
  736. $this->assertFalse($entity->isDirty('tags'), 'Should be clean');
  737. $fresh = $articles->get(1, ['contain' => 'Tags']);
  738. $this->assertCount(3, $fresh->tags, 'Records should be in db');
  739. $this->assertNotEmpty($tags->get(2), 'Unlinked tag should still exist');
  740. }
  741. /**
  742. * Tests that replaceLinks() will contain() the target table when
  743. * there are conditions present on the association.
  744. *
  745. * In this case the replacement will fail because the association conditions
  746. * hide the fixture data.
  747. *
  748. * @return void
  749. */
  750. public function testReplaceLinkWithConditions()
  751. {
  752. $joint = $this->getTableLocator()->get('SpecialTags');
  753. $articles = $this->getTableLocator()->get('Articles');
  754. $tags = $this->getTableLocator()->get('Tags');
  755. $assoc = $articles->belongsToMany('Tags', [
  756. 'sourceTable' => $articles,
  757. 'targetTable' => $tags,
  758. 'through' => $joint,
  759. 'joinTable' => 'special_tags',
  760. 'conditions' => ['SpecialTags.highlighted' => true],
  761. ]);
  762. $entity = $articles->get(1, ['contain' => 'Tags']);
  763. $result = $assoc->replaceLinks($entity, [], ['associated' => false]);
  764. $this->assertTrue($result);
  765. $this->assertSame([], $entity->tags, 'Tags should match replaced objects');
  766. $this->assertFalse($entity->isDirty('tags'), 'Should be clean');
  767. $fresh = $articles->get(1, ['contain' => 'Tags']);
  768. $this->assertCount(0, $fresh->tags, 'Association should be empty');
  769. $jointCount = $joint->find()->where(['article_id' => 1])->count();
  770. $this->assertSame(1, $jointCount, 'Non matching joint record should remain.');
  771. }
  772. /**
  773. * Tests replaceLinks with failing domain rules and new link targets.
  774. *
  775. * @return void
  776. */
  777. public function testReplaceLinkFailingDomainRules()
  778. {
  779. $articles = $this->getTableLocator()->get('Articles');
  780. $tags = $this->getTableLocator()->get('Tags');
  781. $tags->getEventManager()->on('Model.buildRules', function (Event $event, $rules) {
  782. $rules->add(function () {
  783. return false;
  784. }, 'rule', ['errorField' => 'name', 'message' => 'Bad data']);
  785. });
  786. $assoc = $articles->belongsToMany('Tags', [
  787. 'sourceTable' => $articles,
  788. 'targetTable' => $tags,
  789. 'through' => $this->getTableLocator()->get('ArticlesTags'),
  790. 'joinTable' => 'articles_tags',
  791. ]);
  792. $entity = $articles->get(1, ['contain' => 'Tags']);
  793. $originalCount = count($entity->tags);
  794. $tags = [
  795. new Entity(['name' => 'tag99', 'description' => 'Best tag']),
  796. ];
  797. $result = $assoc->replaceLinks($entity, $tags);
  798. $this->assertFalse($result, 'replace should have failed.');
  799. $this->assertNotEmpty($tags[0]->getErrors(), 'Bad entity should have errors.');
  800. $entity = $articles->get(1, ['contain' => 'Tags']);
  801. $this->assertCount($originalCount, $entity->tags, 'Should not have changed.');
  802. $this->assertEquals('tag1', $entity->tags[0]->name);
  803. }
  804. /**
  805. * Provider for empty values
  806. *
  807. * @return array
  808. */
  809. public function emptyProvider()
  810. {
  811. return [
  812. [''],
  813. [false],
  814. [null],
  815. [[]],
  816. ];
  817. }
  818. /**
  819. * Test that saveAssociated() fails on non-empty, non-iterable value
  820. *
  821. * @return void
  822. */
  823. public function testSaveAssociatedNotEmptyNotIterable()
  824. {
  825. $this->expectException(\InvalidArgumentException::class);
  826. $this->expectExceptionMessage('Could not save tags, it cannot be traversed');
  827. $articles = $this->getTableLocator()->get('Articles');
  828. $assoc = $articles->belongsToMany('Tags', [
  829. 'saveStrategy' => BelongsToMany::SAVE_APPEND,
  830. 'joinTable' => 'articles_tags',
  831. ]);
  832. $entity = new Entity([
  833. 'id' => 1,
  834. 'tags' => 'oh noes',
  835. ], ['markNew' => true]);
  836. $assoc->saveAssociated($entity);
  837. }
  838. /**
  839. * Test that saving an empty set on create works.
  840. *
  841. * @dataProvider emptyProvider
  842. * @return void
  843. */
  844. public function testSaveAssociatedEmptySetSuccess($value)
  845. {
  846. $table = $this->getMockBuilder('Cake\ORM\Table')
  847. ->setMethods(['table'])
  848. ->getMock();
  849. $table->setSchema([]);
  850. $assoc = $this->getMockBuilder('\Cake\ORM\Association\BelongsToMany')
  851. ->setMethods(['_saveTarget', 'replaceLinks'])
  852. ->setConstructorArgs(['tags', ['sourceTable' => $table]])
  853. ->getMock();
  854. $entity = new Entity([
  855. 'id' => 1,
  856. 'tags' => $value,
  857. ], ['markNew' => true]);
  858. $assoc->setSaveStrategy(BelongsToMany::SAVE_REPLACE);
  859. $assoc->expects($this->never())
  860. ->method('replaceLinks');
  861. $assoc->expects($this->never())
  862. ->method('_saveTarget');
  863. $this->assertSame($entity, $assoc->saveAssociated($entity));
  864. }
  865. /**
  866. * Test that saving an empty set on update works.
  867. *
  868. * @dataProvider emptyProvider
  869. * @return void
  870. */
  871. public function testSaveAssociatedEmptySetUpdateSuccess($value)
  872. {
  873. $table = $this->getMockBuilder('Cake\ORM\Table')
  874. ->setMethods(['table'])
  875. ->getMock();
  876. $table->setSchema([]);
  877. $assoc = $this->getMockBuilder('\Cake\ORM\Association\BelongsToMany')
  878. ->setMethods(['_saveTarget', 'replaceLinks'])
  879. ->setConstructorArgs(['tags', ['sourceTable' => $table]])
  880. ->getMock();
  881. $entity = new Entity([
  882. 'id' => 1,
  883. 'tags' => $value,
  884. ], ['markNew' => false]);
  885. $assoc->setSaveStrategy(BelongsToMany::SAVE_REPLACE);
  886. $assoc->expects($this->once())
  887. ->method('replaceLinks')
  888. ->with($entity, [])
  889. ->will($this->returnValue(true));
  890. $assoc->expects($this->never())
  891. ->method('_saveTarget');
  892. $this->assertSame($entity, $assoc->saveAssociated($entity));
  893. }
  894. /**
  895. * Tests saving with replace strategy returning true
  896. *
  897. * @return void
  898. */
  899. public function testSaveAssociatedWithReplace()
  900. {
  901. $table = $this->getMockBuilder('Cake\ORM\Table')
  902. ->setMethods(['table'])
  903. ->getMock();
  904. $table->setSchema([]);
  905. $assoc = $this->getMockBuilder('\Cake\ORM\Association\BelongsToMany')
  906. ->setMethods(['replaceLinks'])
  907. ->setConstructorArgs(['tags', ['sourceTable' => $table]])
  908. ->getMock();
  909. $entity = new Entity([
  910. 'id' => 1,
  911. 'tags' => [
  912. new Entity(['name' => 'foo']),
  913. ],
  914. ]);
  915. $options = ['foo' => 'bar'];
  916. $assoc->setSaveStrategy(BelongsToMany::SAVE_REPLACE);
  917. $assoc->expects($this->once())->method('replaceLinks')
  918. ->with($entity, $entity->tags, $options)
  919. ->will($this->returnValue(true));
  920. $this->assertSame($entity, $assoc->saveAssociated($entity, $options));
  921. }
  922. /**
  923. * Tests saving with replace strategy returning true
  924. *
  925. * @return void
  926. */
  927. public function testSaveAssociatedWithReplaceReturnFalse()
  928. {
  929. $table = $this->getMockBuilder('Cake\ORM\Table')
  930. ->setMethods(['table'])
  931. ->getMock();
  932. $table->setSchema([]);
  933. $assoc = $this->getMockBuilder('\Cake\ORM\Association\BelongsToMany')
  934. ->setMethods(['replaceLinks'])
  935. ->setConstructorArgs(['tags', ['sourceTable' => $table]])
  936. ->getMock();
  937. $entity = new Entity([
  938. 'id' => 1,
  939. 'tags' => [
  940. new Entity(['name' => 'foo']),
  941. ],
  942. ]);
  943. $options = ['foo' => 'bar'];
  944. $assoc->setSaveStrategy(BelongsToMany::SAVE_REPLACE);
  945. $assoc->expects($this->once())->method('replaceLinks')
  946. ->with($entity, $entity->tags, $options)
  947. ->will($this->returnValue(false));
  948. $this->assertFalse($assoc->saveAssociated($entity, $options));
  949. }
  950. /**
  951. * Test that saveAssociated() ignores non entity values.
  952. *
  953. * @return void
  954. */
  955. public function testSaveAssociatedOnlyEntitiesAppend()
  956. {
  957. $connection = ConnectionManager::get('test');
  958. $mock = $this->getMockBuilder('Cake\ORM\Table')
  959. ->setMethods(['saveAssociated', 'schema'])
  960. ->setConstructorArgs([['table' => 'tags', 'connection' => $connection]])
  961. ->getMock();
  962. $mock->setPrimaryKey('id');
  963. $config = [
  964. 'sourceTable' => $this->article,
  965. 'targetTable' => $mock,
  966. 'saveStrategy' => BelongsToMany::SAVE_APPEND,
  967. ];
  968. $entity = new Entity([
  969. 'id' => 1,
  970. 'title' => 'First Post',
  971. 'tags' => [
  972. ['tag' => 'nope'],
  973. new Entity(['tag' => 'cakephp']),
  974. ],
  975. ]);
  976. $mock->expects($this->never())
  977. ->method('saveAssociated');
  978. $association = new BelongsToMany('Tags', $config);
  979. $association->saveAssociated($entity);
  980. }
  981. /**
  982. * Tests that targetForeignKey() returns the correct configured value
  983. *
  984. * @group deprecated
  985. * @return void
  986. */
  987. public function testTargetForeignKey()
  988. {
  989. $this->deprecated(function () {
  990. $assoc = new BelongsToMany('Test', [
  991. 'sourceTable' => $this->article,
  992. 'targetTable' => $this->tag,
  993. ]);
  994. $this->assertEquals('tag_id', $assoc->targetForeignKey());
  995. $this->assertEquals('another_key', $assoc->targetForeignKey('another_key'));
  996. $this->assertEquals('another_key', $assoc->targetForeignKey());
  997. $assoc = new BelongsToMany('Test', [
  998. 'sourceTable' => $this->article,
  999. 'targetTable' => $this->tag,
  1000. 'targetForeignKey' => 'foo',
  1001. ]);
  1002. $this->assertEquals('foo', $assoc->targetForeignKey());
  1003. });
  1004. }
  1005. /**
  1006. * Tests that setTargetForeignKey() returns the correct configured value
  1007. *
  1008. * @return void
  1009. */
  1010. public function testSetTargetForeignKey()
  1011. {
  1012. $assoc = new BelongsToMany('Test', [
  1013. 'sourceTable' => $this->article,
  1014. 'targetTable' => $this->tag,
  1015. ]);
  1016. $this->assertEquals('tag_id', $assoc->getTargetForeignKey());
  1017. $assoc->setTargetForeignKey('another_key');
  1018. $this->assertEquals('another_key', $assoc->getTargetForeignKey());
  1019. $assoc = new BelongsToMany('Test', [
  1020. 'sourceTable' => $this->article,
  1021. 'targetTable' => $this->tag,
  1022. 'targetForeignKey' => 'foo',
  1023. ]);
  1024. $this->assertEquals('foo', $assoc->getTargetForeignKey());
  1025. }
  1026. /**
  1027. * Tests that custom foreignKeys are properly transmitted to involved associations
  1028. * when they are customized
  1029. *
  1030. * @return void
  1031. */
  1032. public function testJunctionWithCustomForeignKeys()
  1033. {
  1034. $assoc = new BelongsToMany('Test', [
  1035. 'sourceTable' => $this->article,
  1036. 'targetTable' => $this->tag,
  1037. 'foreignKey' => 'Art',
  1038. 'targetForeignKey' => 'Tag',
  1039. ]);
  1040. $junction = $assoc->junction();
  1041. $this->assertEquals('Art', $junction->getAssociation('Articles')->getForeignKey());
  1042. $this->assertEquals('Tag', $junction->getAssociation('Tags')->getForeignKey());
  1043. $inverseRelation = $this->tag->getAssociation('Articles');
  1044. $this->assertEquals('Tag', $inverseRelation->getForeignKey());
  1045. $this->assertEquals('Art', $inverseRelation->getTargetForeignKey());
  1046. }
  1047. /**
  1048. * Tests that property is being set using the constructor options.
  1049. *
  1050. * @return void
  1051. */
  1052. public function testPropertyOption()
  1053. {
  1054. $config = ['propertyName' => 'thing_placeholder'];
  1055. $association = new BelongsToMany('Thing', $config);
  1056. $this->assertEquals('thing_placeholder', $association->getProperty());
  1057. }
  1058. /**
  1059. * Test that plugin names are omitted from property()
  1060. *
  1061. * @return void
  1062. */
  1063. public function testPropertyNoPlugin()
  1064. {
  1065. $mock = $this->getMockBuilder('Cake\ORM\Table')
  1066. ->disableOriginalConstructor()
  1067. ->getMock();
  1068. $config = [
  1069. 'sourceTable' => $this->article,
  1070. 'targetTable' => $mock,
  1071. ];
  1072. $association = new BelongsToMany('Contacts.Tags', $config);
  1073. $this->assertEquals('tags', $association->getProperty());
  1074. }
  1075. /**
  1076. * Test that the generated associations are correct.
  1077. *
  1078. * @return void
  1079. */
  1080. public function testGeneratedAssociations()
  1081. {
  1082. $articles = $this->getTableLocator()->get('Articles');
  1083. $tags = $this->getTableLocator()->get('Tags');
  1084. $conditions = ['SpecialTags.highlighted' => true];
  1085. $assoc = $articles->belongsToMany('Tags', [
  1086. 'sourceTable' => $articles,
  1087. 'targetTable' => $tags,
  1088. 'foreignKey' => 'foreign_key',
  1089. 'targetForeignKey' => 'target_foreign_key',
  1090. 'through' => 'SpecialTags',
  1091. 'conditions' => $conditions,
  1092. ]);
  1093. // Generate associations
  1094. $assoc->junction();
  1095. $tagAssoc = $articles->getAssociation('Tags');
  1096. $this->assertNotEmpty($tagAssoc, 'btm should exist');
  1097. $this->assertEquals($conditions, $tagAssoc->getConditions());
  1098. $this->assertEquals('target_foreign_key', $tagAssoc->getTargetForeignKey());
  1099. $this->assertEquals('foreign_key', $tagAssoc->getForeignKey());
  1100. $jointAssoc = $articles->getAssociation('SpecialTags');
  1101. $this->assertNotEmpty($jointAssoc, 'has many to junction should exist');
  1102. $this->assertInstanceOf('Cake\ORM\Association\HasMany', $jointAssoc);
  1103. $this->assertEquals('foreign_key', $jointAssoc->getForeignKey());
  1104. $articleAssoc = $tags->getAssociation('Articles');
  1105. $this->assertNotEmpty($articleAssoc, 'reverse btm should exist');
  1106. $this->assertInstanceOf('Cake\ORM\Association\BelongsToMany', $articleAssoc);
  1107. $this->assertEquals($conditions, $articleAssoc->getConditions());
  1108. $this->assertEquals('foreign_key', $articleAssoc->getTargetForeignKey(), 'keys should swap');
  1109. $this->assertEquals('target_foreign_key', $articleAssoc->getForeignKey(), 'keys should swap');
  1110. $jointAssoc = $tags->getAssociation('SpecialTags');
  1111. $this->assertNotEmpty($jointAssoc, 'has many to junction should exist');
  1112. $this->assertInstanceOf('Cake\ORM\Association\HasMany', $jointAssoc);
  1113. $this->assertEquals('target_foreign_key', $jointAssoc->getForeignKey());
  1114. }
  1115. /**
  1116. * Tests that eager loading requires association keys
  1117. *
  1118. * @return void
  1119. */
  1120. public function testEagerLoadingRequiresPrimaryKey()
  1121. {
  1122. $this->expectException(\RuntimeException::class);
  1123. $this->expectExceptionMessage('The "tags" table does not define a primary key');
  1124. $table = $this->getTableLocator()->get('Articles');
  1125. $tags = $this->getTableLocator()->get('Tags');
  1126. $tags->getSchema()->dropConstraint('primary');
  1127. $table->belongsToMany('Tags');
  1128. $table->find()->contain('Tags')->first();
  1129. }
  1130. /**
  1131. * Tests that fetching belongsToMany association will not force
  1132. * all fields being returned, but instead will honor the select() clause
  1133. *
  1134. * @see https://github.com/cakephp/cakephp/issues/7916
  1135. * @return void
  1136. */
  1137. public function testEagerLoadingBelongsToManyLimitedFields()
  1138. {
  1139. $table = $this->getTableLocator()->get('Articles');
  1140. $table->belongsToMany('Tags');
  1141. $result = $table
  1142. ->find()
  1143. ->contain(['Tags' => function ($q) {
  1144. return $q->select(['id']);
  1145. }])
  1146. ->first();
  1147. $this->assertNotEmpty($result->tags[0]->id);
  1148. $this->assertEmpty($result->tags[0]->name);
  1149. $result = $table
  1150. ->find()
  1151. ->contain([
  1152. 'Tags' => [
  1153. 'fields' => [
  1154. 'Tags.name',
  1155. ],
  1156. ],
  1157. ])
  1158. ->first();
  1159. $this->assertNotEmpty($result->tags[0]->name);
  1160. $this->assertEmpty($result->tags[0]->id);
  1161. }
  1162. /**
  1163. * Tests that fetching belongsToMany association will retain autoFields(true) if it was used.
  1164. *
  1165. * @see https://github.com/cakephp/cakephp/issues/8052
  1166. * @return void
  1167. */
  1168. public function testEagerLoadingBelongsToManyLimitedFieldsWithAutoFields()
  1169. {
  1170. $table = $this->getTableLocator()->get('Articles');
  1171. $table->belongsToMany('Tags');
  1172. $result = $table
  1173. ->find()
  1174. ->contain(['Tags' => function ($q) {
  1175. return $q->select(['two' => $q->newExpr('1 + 1')])->enableAutoFields(true);
  1176. }])
  1177. ->first();
  1178. $this->assertNotEmpty($result->tags[0]->two, 'Should have computed field');
  1179. $this->assertNotEmpty($result->tags[0]->name, 'Should have standard field');
  1180. }
  1181. /**
  1182. * Test that association proxy find() applies joins when conditions are involved.
  1183. *
  1184. * @return void
  1185. */
  1186. public function testAssociationProxyFindWithConditions()
  1187. {
  1188. $table = $this->getTableLocator()->get('Articles');
  1189. $table->belongsToMany('Tags', [
  1190. 'foreignKey' => 'article_id',
  1191. 'associationForeignKey' => 'tag_id',
  1192. 'conditions' => ['SpecialTags.highlighted' => true],
  1193. 'through' => 'SpecialTags',
  1194. ]);
  1195. $query = $table->Tags->find();
  1196. $result = $query->toArray();
  1197. $this->assertCount(1, $result);
  1198. $this->assertEquals(1, $result[0]->id);
  1199. }
  1200. /**
  1201. * Test that association proxy find() applies complex conditions
  1202. *
  1203. * @return void
  1204. */
  1205. public function testAssociationProxyFindWithComplexConditions()
  1206. {
  1207. $table = $this->getTableLocator()->get('Articles');
  1208. $table->belongsToMany('Tags', [
  1209. 'foreignKey' => 'article_id',
  1210. 'associationForeignKey' => 'tag_id',
  1211. 'conditions' => [
  1212. 'OR' => [
  1213. 'SpecialTags.highlighted' => true,
  1214. ],
  1215. ],
  1216. 'through' => 'SpecialTags',
  1217. ]);
  1218. $query = $table->Tags->find();
  1219. $result = $query->toArray();
  1220. $this->assertCount(1, $result);
  1221. $this->assertEquals(1, $result[0]->id);
  1222. }
  1223. /**
  1224. * Test that matching() works on belongsToMany associations.
  1225. *
  1226. * @return void
  1227. */
  1228. public function testBelongsToManyAssociationWithArrayConditions()
  1229. {
  1230. $table = $this->getTableLocator()->get('Articles');
  1231. $table->belongsToMany('Tags', [
  1232. 'foreignKey' => 'article_id',
  1233. 'associationForeignKey' => 'tag_id',
  1234. 'conditions' => ['SpecialTags.highlighted' => true],
  1235. 'through' => 'SpecialTags',
  1236. ]);
  1237. $query = $table->find()->matching('Tags', function ($q) {
  1238. return $q->where(['Tags.name' => 'tag1']);
  1239. });
  1240. $results = $query->toArray();
  1241. $this->assertCount(1, $results);
  1242. $this->assertNotEmpty($results[0]->_matchingData);
  1243. }
  1244. /**
  1245. * Test that matching() works on belongsToMany associations.
  1246. *
  1247. * @return void
  1248. */
  1249. public function testBelongsToManyAssociationWithExpressionConditions()
  1250. {
  1251. $table = $this->getTableLocator()->get('Articles');
  1252. $table->belongsToMany('Tags', [
  1253. 'foreignKey' => 'article_id',
  1254. 'associationForeignKey' => 'tag_id',
  1255. 'conditions' => [new QueryExpression("name LIKE 'tag%'")],
  1256. 'through' => 'SpecialTags',
  1257. ]);
  1258. $query = $table->find()->matching('Tags', function ($q) {
  1259. return $q->where(['Tags.name' => 'tag1']);
  1260. });
  1261. $results = $query->toArray();
  1262. $this->assertCount(1, $results);
  1263. $this->assertNotEmpty($results[0]->_matchingData);
  1264. }
  1265. /**
  1266. * Test that association proxy find() with matching resolves joins correctly
  1267. *
  1268. * @return void
  1269. */
  1270. public function testAssociationProxyFindWithConditionsMatching()
  1271. {
  1272. $table = $this->getTableLocator()->get('Articles');
  1273. $table->belongsToMany('Tags', [
  1274. 'foreignKey' => 'article_id',
  1275. 'associationForeignKey' => 'tag_id',
  1276. 'conditions' => ['SpecialTags.highlighted' => true],
  1277. 'through' => 'SpecialTags',
  1278. ]);
  1279. $query = $table->Tags->find()->matching('Articles', function ($query) {
  1280. return $query->where(['Articles.id' => 1]);
  1281. });
  1282. // The inner join on special_tags excludes the results.
  1283. $this->assertEquals(0, $query->count());
  1284. }
  1285. /**
  1286. * Test custom binding key for target table association
  1287. *
  1288. * @return void
  1289. */
  1290. public function testCustomTargetBindingKeyContain()
  1291. {
  1292. $this->getTableLocator()->get('ArticlesTags')
  1293. ->belongsTo('SpecialTags', [
  1294. 'bindingKey' => 'tag_id',
  1295. 'foreignKey' => 'tag_id',
  1296. ]);
  1297. $table = $this->getTableLocator()->get('Articles');
  1298. $table->belongsToMany('SpecialTags', [
  1299. 'through' => 'ArticlesTags',
  1300. 'targetForeignKey' => 'tag_id',
  1301. ]);
  1302. $results = $table->find()
  1303. ->contain('SpecialTags', function ($query) {
  1304. return $query->order(['SpecialTags.tag_id']);
  1305. })
  1306. ->where(['id' => 2])
  1307. ->toArray();
  1308. $this->assertCount(1, $results);
  1309. $this->assertCount(2, $results[0]->special_tags);
  1310. $this->assertSame(2, $results[0]->special_tags[0]->id);
  1311. $this->assertSame(1, $results[0]->special_tags[0]->tag_id);
  1312. $this->assertSame(1, $results[0]->special_tags[1]->id);
  1313. $this->assertSame(3, $results[0]->special_tags[1]->tag_id);
  1314. }
  1315. /**
  1316. * Test custom binding key for target table association
  1317. *
  1318. * @return void
  1319. */
  1320. public function testCustomTargetBindingKeyLink()
  1321. {
  1322. $this->getTableLocator()->get('ArticlesTags')
  1323. ->belongsTo('SpecialTags', [
  1324. 'bindingKey' => 'tag_id',
  1325. 'foreignKey' => 'tag_id',
  1326. ]);
  1327. $table = $this->getTableLocator()->get('Articles');
  1328. $table->belongsToMany('SpecialTags', [
  1329. 'through' => 'ArticlesTags',
  1330. 'targetForeignKey' => 'tag_id',
  1331. ]);
  1332. $specialTag = $table->SpecialTags->newEntity([
  1333. 'article_id' => 2,
  1334. 'tag_id' => 2,
  1335. ]);
  1336. $table->SpecialTags->save($specialTag);
  1337. $article = $table->get(2);
  1338. $this->assertTrue($table->SpecialTags->link($article, [$specialTag]));
  1339. $results = $table->find()
  1340. ->contain('SpecialTags')
  1341. ->where(['id' => 2])
  1342. ->toArray();
  1343. $this->assertCount(1, $results);
  1344. $this->assertCount(3, $results[0]->special_tags);
  1345. }
  1346. }