HasManyTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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\ORM\Association\HasMany;
  21. use Cake\ORM\Entity;
  22. use Cake\ORM\Query;
  23. use Cake\ORM\Table;
  24. use Cake\ORM\TableRegistry;
  25. use Cake\TestSuite\TestCase;
  26. /**
  27. * Tests HasMany class
  28. *
  29. */
  30. class HasManyTest extends TestCase
  31. {
  32. /**
  33. * Set up
  34. *
  35. * @return void
  36. */
  37. public function setUp()
  38. {
  39. parent::setUp();
  40. $this->author = TableRegistry::get('Authors', [
  41. 'schema' => [
  42. 'id' => ['type' => 'integer'],
  43. 'username' => ['type' => 'string'],
  44. '_constraints' => [
  45. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  46. ]
  47. ]
  48. ]);
  49. $this->article = $this->getMock(
  50. 'Cake\ORM\Table',
  51. ['find', 'deleteAll', 'delete'],
  52. [['alias' => 'Articles', 'table' => 'articles']]
  53. );
  54. $this->article->schema([
  55. 'id' => ['type' => 'integer'],
  56. 'title' => ['type' => 'string'],
  57. 'author_id' => ['type' => 'integer'],
  58. '_constraints' => [
  59. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  60. ]
  61. ]);
  62. $this->articlesTypeMap = new TypeMap([
  63. 'Articles.id' => 'integer',
  64. 'id' => 'integer',
  65. 'Articles.title' => 'string',
  66. 'title' => 'string',
  67. 'Articles.author_id' => 'integer',
  68. 'author_id' => 'integer',
  69. ]);
  70. }
  71. /**
  72. * Tear down
  73. *
  74. * @return void
  75. */
  76. public function tearDown()
  77. {
  78. parent::tearDown();
  79. TableRegistry::clear();
  80. }
  81. /**
  82. * Tests that the association reports it can be joined
  83. *
  84. * @return void
  85. */
  86. public function testCanBeJoined()
  87. {
  88. $assoc = new HasMany('Test');
  89. $this->assertFalse($assoc->canBeJoined());
  90. }
  91. /**
  92. * Tests sort() method
  93. *
  94. * @return void
  95. */
  96. public function testSort()
  97. {
  98. $assoc = new HasMany('Test');
  99. $this->assertNull($assoc->sort());
  100. $assoc->sort(['id' => 'ASC']);
  101. $this->assertEquals(['id' => 'ASC'], $assoc->sort());
  102. }
  103. /**
  104. * Tests requiresKeys() method
  105. *
  106. * @return void
  107. */
  108. public function testRequiresKeys()
  109. {
  110. $assoc = new HasMany('Test');
  111. $this->assertTrue($assoc->requiresKeys());
  112. $assoc->strategy(HasMany::STRATEGY_SUBQUERY);
  113. $this->assertFalse($assoc->requiresKeys());
  114. $assoc->strategy(HasMany::STRATEGY_SELECT);
  115. $this->assertTrue($assoc->requiresKeys());
  116. }
  117. /**
  118. * Tests that HasMany can't use the join strategy
  119. *
  120. * @expectedException \InvalidArgumentException
  121. * @expectedExceptionMessage Invalid strategy "join" was provided
  122. * @return void
  123. */
  124. public function testStrategyFailure()
  125. {
  126. $assoc = new HasMany('Test');
  127. $assoc->strategy(HasMany::STRATEGY_JOIN);
  128. }
  129. /**
  130. * Test the eager loader method with no extra options
  131. *
  132. * @return void
  133. */
  134. public function testEagerLoader()
  135. {
  136. $config = [
  137. 'sourceTable' => $this->author,
  138. 'targetTable' => $this->article,
  139. 'strategy' => 'select'
  140. ];
  141. $association = new HasMany('Articles', $config);
  142. $keys = [1, 2, 3, 4];
  143. $query = $this->getMock('Cake\ORM\Query', ['all'], [null, null]);
  144. $this->article->expects($this->once())->method('find')->with('all')
  145. ->will($this->returnValue($query));
  146. $results = [
  147. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  148. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  149. ];
  150. $query->expects($this->once())->method('all')
  151. ->will($this->returnValue($results));
  152. $callable = $association->eagerLoader(compact('keys', 'query'));
  153. $row = ['Authors__id' => 1, 'username' => 'author 1'];
  154. $result = $callable($row);
  155. $row['Articles'] = [
  156. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  157. ];
  158. $this->assertEquals($row, $result);
  159. $row = ['Authors__id' => 2, 'username' => 'author 2'];
  160. $result = $callable($row);
  161. $row['Articles'] = [
  162. ['id' => 1, 'title' => 'article 1', 'author_id' => 2]
  163. ];
  164. $this->assertEquals($row, $result);
  165. }
  166. /**
  167. * Test the eager loader method with default query clauses
  168. *
  169. * @return void
  170. */
  171. public function testEagerLoaderWithDefaults()
  172. {
  173. $config = [
  174. 'sourceTable' => $this->author,
  175. 'targetTable' => $this->article,
  176. 'conditions' => ['Articles.is_active' => true],
  177. 'sort' => ['id' => 'ASC'],
  178. 'strategy' => 'select'
  179. ];
  180. $association = new HasMany('Articles', $config);
  181. $keys = [1, 2, 3, 4];
  182. $query = $this->getMock(
  183. 'Cake\ORM\Query',
  184. ['all', 'where', 'andWhere', 'order'],
  185. [null, null]
  186. );
  187. $this->article->expects($this->once())->method('find')->with('all')
  188. ->will($this->returnValue($query));
  189. $results = [
  190. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  191. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  192. ];
  193. $query->expects($this->once())->method('all')
  194. ->will($this->returnValue($results));
  195. $query->expects($this->at(0))->method('where')
  196. ->with(['Articles.is_active' => true])
  197. ->will($this->returnSelf());
  198. $query->expects($this->at(1))->method('where')
  199. ->with([])
  200. ->will($this->returnSelf());
  201. $query->expects($this->once())->method('andWhere')
  202. ->with(['Articles.author_id IN' => $keys])
  203. ->will($this->returnSelf());
  204. $query->expects($this->once())->method('order')
  205. ->with(['id' => 'ASC'])
  206. ->will($this->returnSelf());
  207. $association->eagerLoader(compact('keys', 'query'));
  208. }
  209. /**
  210. * Test the eager loader method with overridden query clauses
  211. *
  212. * @return void
  213. */
  214. public function testEagerLoaderWithOverrides()
  215. {
  216. $config = [
  217. 'sourceTable' => $this->author,
  218. 'targetTable' => $this->article,
  219. 'conditions' => ['Articles.is_active' => true],
  220. 'sort' => ['id' => 'ASC'],
  221. 'strategy' => 'select'
  222. ];
  223. $association = new HasMany('Articles', $config);
  224. $keys = [1, 2, 3, 4];
  225. $query = $this->getMock(
  226. 'Cake\ORM\Query',
  227. ['all', 'where', 'andWhere', 'order', 'select', 'contain'],
  228. [null, null]
  229. );
  230. $this->article->expects($this->once())->method('find')->with('all')
  231. ->will($this->returnValue($query));
  232. $results = [
  233. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  234. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  235. ];
  236. $query->expects($this->once())->method('all')
  237. ->will($this->returnValue($results));
  238. $query->expects($this->at(0))->method('where')
  239. ->with(['Articles.is_active' => true])
  240. ->will($this->returnSelf());
  241. $query->expects($this->at(1))->method('where')
  242. ->with(['Articles.id !=' => 3])
  243. ->will($this->returnSelf());
  244. $query->expects($this->once())->method('andWhere')
  245. ->with(['Articles.author_id IN' => $keys])
  246. ->will($this->returnSelf());
  247. $query->expects($this->once())->method('order')
  248. ->with(['title' => 'DESC'])
  249. ->will($this->returnSelf());
  250. $query->expects($this->once())->method('select')
  251. ->with([
  252. 'Articles__title' => 'Articles.title',
  253. 'Articles__author_id' => 'Articles.author_id'
  254. ])
  255. ->will($this->returnSelf());
  256. $query->expects($this->once())->method('contain')
  257. ->with([
  258. 'Categories' => ['fields' => ['a', 'b']],
  259. ])
  260. ->will($this->returnSelf());
  261. $association->eagerLoader([
  262. 'conditions' => ['Articles.id !=' => 3],
  263. 'sort' => ['title' => 'DESC'],
  264. 'fields' => ['title', 'author_id'],
  265. 'contain' => ['Categories' => ['fields' => ['a', 'b']]],
  266. 'keys' => $keys,
  267. 'query' => $query
  268. ]);
  269. }
  270. /**
  271. * Test that failing to add the foreignKey to the list of fields will throw an
  272. * exception
  273. *
  274. * @expectedException \InvalidArgumentException
  275. * @expectedExceptionMessage You are required to select the "Articles.author_id"
  276. * @return void
  277. */
  278. public function testEagerLoaderFieldsException()
  279. {
  280. $config = [
  281. 'sourceTable' => $this->author,
  282. 'targetTable' => $this->article,
  283. 'strategy' => 'select'
  284. ];
  285. $association = new HasMany('Articles', $config);
  286. $keys = [1, 2, 3, 4];
  287. $query = $this->getMock(
  288. 'Cake\ORM\Query',
  289. ['all'],
  290. [null, null]
  291. );
  292. $this->article->expects($this->once())->method('find')->with('all')
  293. ->will($this->returnValue($query));
  294. $association->eagerLoader([
  295. 'fields' => ['id', 'title'],
  296. 'keys' => $keys,
  297. 'query' => $query
  298. ]);
  299. }
  300. /**
  301. * Tests that eager loader accepts a queryBuilder option
  302. *
  303. * @return void
  304. */
  305. public function testEagerLoaderWithQueryBuilder()
  306. {
  307. $config = [
  308. 'sourceTable' => $this->author,
  309. 'targetTable' => $this->article,
  310. 'strategy' => 'select'
  311. ];
  312. $association = new HasMany('Articles', $config);
  313. $keys = [1, 2, 3, 4];
  314. $query = $this->getMock(
  315. 'Cake\ORM\Query',
  316. ['all', 'select', 'join', 'where'],
  317. [null, null]
  318. );
  319. $this->article->expects($this->once())->method('find')->with('all')
  320. ->will($this->returnValue($query));
  321. $results = [
  322. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  323. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  324. ];
  325. $query->expects($this->once())->method('all')
  326. ->will($this->returnValue($results));
  327. $query->expects($this->any())->method('select')
  328. ->will($this->returnSelf());
  329. $query->expects($this->at(2))->method('select')
  330. ->with(['a', 'b'])
  331. ->will($this->returnSelf());
  332. $query->expects($this->at(3))->method('join')
  333. ->with('foo')
  334. ->will($this->returnSelf());
  335. $query->expects($this->any())->method('where')
  336. ->will($this->returnSelf());
  337. $query->expects($this->at(4))->method('where')
  338. ->with(['a' => 1])
  339. ->will($this->returnSelf());
  340. $queryBuilder = function ($query) {
  341. return $query->select(['a', 'b'])->join('foo')->where(['a' => 1]);
  342. };
  343. $association->eagerLoader(compact('keys', 'query', 'queryBuilder'));
  344. }
  345. /**
  346. * Test the eager loader method with no extra options
  347. *
  348. * @return void
  349. */
  350. public function testEagerLoaderMultipleKeys()
  351. {
  352. $config = [
  353. 'sourceTable' => $this->author,
  354. 'targetTable' => $this->article,
  355. 'strategy' => 'select',
  356. 'foreignKey' => ['author_id', 'site_id']
  357. ];
  358. $this->author->primaryKey(['id', 'site_id']);
  359. $association = new HasMany('Articles', $config);
  360. $keys = [[1, 10], [2, 20], [3, 30], [4, 40]];
  361. $query = $this->getMock('Cake\ORM\Query', ['all', 'andWhere'], [null, null]);
  362. $this->article->expects($this->once())->method('find')->with('all')
  363. ->will($this->returnValue($query));
  364. $results = [
  365. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10],
  366. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20]
  367. ];
  368. $query->expects($this->once())->method('all')
  369. ->will($this->returnValue($results));
  370. $tuple = new TupleComparison(
  371. ['Articles.author_id', 'Articles.site_id'],
  372. $keys,
  373. [],
  374. 'IN'
  375. );
  376. $query->expects($this->once())->method('andWhere')
  377. ->with($tuple)
  378. ->will($this->returnSelf());
  379. $callable = $association->eagerLoader(compact('keys', 'query'));
  380. $row = ['Authors__id' => 2, 'Authors__site_id' => 10, 'username' => 'author 1'];
  381. $result = $callable($row);
  382. $row['Articles'] = [
  383. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10]
  384. ];
  385. $this->assertEquals($row, $result);
  386. $row = ['Authors__id' => 1, 'username' => 'author 2', 'Authors__site_id' => 20];
  387. $result = $callable($row);
  388. $row['Articles'] = [
  389. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20]
  390. ];
  391. $this->assertEquals($row, $result);
  392. }
  393. /**
  394. * Test cascading deletes.
  395. *
  396. * @return void
  397. */
  398. public function testCascadeDelete()
  399. {
  400. $config = [
  401. 'dependent' => true,
  402. 'sourceTable' => $this->author,
  403. 'targetTable' => $this->article,
  404. 'conditions' => ['Articles.is_active' => true],
  405. ];
  406. $association = new HasMany('Articles', $config);
  407. $this->article->expects($this->once())
  408. ->method('deleteAll')
  409. ->with([
  410. 'Articles.is_active' => true,
  411. 'author_id' => 1
  412. ]);
  413. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  414. $association->cascadeDelete($entity);
  415. }
  416. /**
  417. * Test cascading delete with has many.
  418. *
  419. * @return void
  420. */
  421. public function testCascadeDeleteCallbacks()
  422. {
  423. $config = [
  424. 'dependent' => true,
  425. 'sourceTable' => $this->author,
  426. 'targetTable' => $this->article,
  427. 'conditions' => ['Articles.is_active' => true],
  428. 'cascadeCallbacks' => true,
  429. ];
  430. $association = new HasMany('Articles', $config);
  431. $articleOne = new Entity(['id' => 2, 'title' => 'test']);
  432. $articleTwo = new Entity(['id' => 3, 'title' => 'testing']);
  433. $iterator = new \ArrayIterator([
  434. $articleOne,
  435. $articleTwo
  436. ]);
  437. $query = $this->getMock('\Cake\ORM\Query', [], [], '', false);
  438. $query->expects($this->at(0))
  439. ->method('where')
  440. ->with(['Articles.is_active' => true])
  441. ->will($this->returnSelf());
  442. $query->expects($this->at(1))
  443. ->method('where')
  444. ->with(['author_id' => 1])
  445. ->will($this->returnSelf());
  446. $query->expects($this->any())
  447. ->method('getIterator')
  448. ->will($this->returnValue($iterator));
  449. $query->expects($this->never())
  450. ->method('bufferResults');
  451. $this->article->expects($this->once())
  452. ->method('find')
  453. ->will($this->returnValue($query));
  454. $this->article->expects($this->at(1))
  455. ->method('delete')
  456. ->with($articleOne, []);
  457. $this->article->expects($this->at(2))
  458. ->method('delete')
  459. ->with($articleTwo, []);
  460. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  461. $this->assertTrue($association->cascadeDelete($entity));
  462. }
  463. /**
  464. * Test that saveAssociated() ignores non entity values.
  465. *
  466. * @return void
  467. */
  468. public function testSaveAssociatedOnlyEntities()
  469. {
  470. $mock = $this->getMock('Cake\ORM\Table', [], [], '', false);
  471. $config = [
  472. 'sourceTable' => $this->author,
  473. 'targetTable' => $mock,
  474. ];
  475. $entity = new Entity([
  476. 'username' => 'Mark',
  477. 'email' => 'mark@example.com',
  478. 'articles' => [
  479. ['title' => 'First Post'],
  480. new Entity(['title' => 'Second Post']),
  481. ]
  482. ]);
  483. $mock->expects($this->never())
  484. ->method('saveAssociated');
  485. $association = new HasMany('Articles', $config);
  486. $association->saveAssociated($entity);
  487. }
  488. /**
  489. * Tests that property is being set using the constructor options.
  490. *
  491. * @return void
  492. */
  493. public function testPropertyOption()
  494. {
  495. $config = ['propertyName' => 'thing_placeholder'];
  496. $association = new hasMany('Thing', $config);
  497. $this->assertEquals('thing_placeholder', $association->property());
  498. }
  499. /**
  500. * Test that plugin names are omitted from property()
  501. *
  502. * @return void
  503. */
  504. public function testPropertyNoPlugin()
  505. {
  506. $mock = $this->getMock('Cake\ORM\Table', [], [], '', false);
  507. $config = [
  508. 'sourceTable' => $this->author,
  509. 'targetTable' => $mock,
  510. ];
  511. $association = new HasMany('Contacts.Addresses', $config);
  512. $this->assertEquals('addresses', $association->property());
  513. }
  514. }