HasManyTest.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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 MIT License (http://www.opensource.org/licenses/mit-license.php)
  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. /**
  26. * Tests HasMany class
  27. *
  28. */
  29. class HasManyTest extends \Cake\TestSuite\TestCase {
  30. /**
  31. * Set up
  32. *
  33. * @return void
  34. */
  35. public function setUp() {
  36. parent::setUp();
  37. $this->author = TableRegistry::get('Authors', [
  38. 'schema' => [
  39. 'id' => ['type' => 'integer'],
  40. 'username' => ['type' => 'string'],
  41. '_constraints' => [
  42. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  43. ]
  44. ]
  45. ]);
  46. $this->article = $this->getMock(
  47. 'Cake\ORM\Table',
  48. ['find', 'deleteAll', 'delete'],
  49. [['alias' => 'Articles', 'table' => 'articles']]
  50. );
  51. $this->article->schema([
  52. 'id' => ['type' => 'integer'],
  53. 'title' => ['type' => 'string'],
  54. 'author_id' => ['type' => 'integer'],
  55. '_constraints' => [
  56. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  57. ]
  58. ]);
  59. $this->articlesTypeMap = new TypeMap([
  60. 'Articles.id' => 'integer',
  61. 'id' => 'integer',
  62. 'Articles.title' => 'string',
  63. 'title' => 'string',
  64. 'Articles.author_id' => 'integer',
  65. 'author_id' => 'integer',
  66. ]);
  67. }
  68. /**
  69. * Tear down
  70. *
  71. * @return void
  72. */
  73. public function tearDown() {
  74. parent::tearDown();
  75. TableRegistry::clear();
  76. }
  77. /**
  78. * Tests that the association reports it can be joined
  79. *
  80. * @return void
  81. */
  82. public function testCanBeJoined() {
  83. $assoc = new HasMany('Test');
  84. $this->assertFalse($assoc->canBeJoined());
  85. }
  86. /**
  87. * Tests sort() method
  88. *
  89. * @return void
  90. */
  91. public function testSort() {
  92. $assoc = new HasMany('Test');
  93. $this->assertNull($assoc->sort());
  94. $assoc->sort(['id' => 'ASC']);
  95. $this->assertEquals(['id' => 'ASC'], $assoc->sort());
  96. }
  97. /**
  98. * Tests requiresKeys() method
  99. *
  100. * @return void
  101. */
  102. public function testRequiresKeys() {
  103. $assoc = new HasMany('Test');
  104. $this->assertTrue($assoc->requiresKeys());
  105. $assoc->strategy(HasMany::STRATEGY_SUBQUERY);
  106. $this->assertFalse($assoc->requiresKeys());
  107. $assoc->strategy(HasMany::STRATEGY_SELECT);
  108. $this->assertTrue($assoc->requiresKeys());
  109. }
  110. /**
  111. * Test the eager loader method with no extra options
  112. *
  113. * @return void
  114. */
  115. public function testEagerLoader() {
  116. $config = [
  117. 'sourceTable' => $this->author,
  118. 'targetTable' => $this->article,
  119. 'strategy' => 'select'
  120. ];
  121. $association = new HasMany('Articles', $config);
  122. $keys = [1, 2, 3, 4];
  123. $query = $this->getMock('Cake\ORM\Query', ['all'], [null, null]);
  124. $this->article->expects($this->once())->method('find')->with('all')
  125. ->will($this->returnValue($query));
  126. $results = [
  127. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  128. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  129. ];
  130. $query->expects($this->once())->method('all')
  131. ->will($this->returnValue($results));
  132. $callable = $association->eagerLoader(compact('keys', 'query'));
  133. $row = ['Authors__id' => 1, 'username' => 'author 1'];
  134. $result = $callable($row);
  135. $row['Articles'] = [
  136. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  137. ];
  138. $this->assertEquals($row, $result);
  139. $row = ['Authors__id' => 2, 'username' => 'author 2'];
  140. $result = $callable($row);
  141. $row['Articles'] = [
  142. ['id' => 1, 'title' => 'article 1', 'author_id' => 2]
  143. ];
  144. $this->assertEquals($row, $result);
  145. }
  146. /**
  147. * Test the eager loader method with default query clauses
  148. *
  149. * @return void
  150. */
  151. public function testEagerLoaderWithDefaults() {
  152. $config = [
  153. 'sourceTable' => $this->author,
  154. 'targetTable' => $this->article,
  155. 'conditions' => ['Articles.is_active' => true],
  156. 'sort' => ['id' => 'ASC'],
  157. 'strategy' => 'select'
  158. ];
  159. $association = new HasMany('Articles', $config);
  160. $keys = [1, 2, 3, 4];
  161. $query = $this->getMock(
  162. 'Cake\ORM\Query',
  163. ['all', 'where', 'andWhere', 'order'],
  164. [null, null]
  165. );
  166. $this->article->expects($this->once())->method('find')->with('all')
  167. ->will($this->returnValue($query));
  168. $results = [
  169. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  170. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  171. ];
  172. $query->expects($this->once())->method('all')
  173. ->will($this->returnValue($results));
  174. $query->expects($this->at(0))->method('where')
  175. ->with(['Articles.is_active' => true])
  176. ->will($this->returnSelf());
  177. $query->expects($this->at(1))->method('where')
  178. ->with([])
  179. ->will($this->returnSelf());
  180. $query->expects($this->once())->method('andWhere')
  181. ->with(['Articles.author_id IN' => $keys])
  182. ->will($this->returnSelf());
  183. $query->expects($this->once())->method('order')
  184. ->with(['id' => 'ASC'])
  185. ->will($this->returnSelf());
  186. $association->eagerLoader(compact('keys', 'query'));
  187. }
  188. /**
  189. * Test the eager loader method with overridden query clauses
  190. *
  191. * @return void
  192. */
  193. public function testEagerLoaderWithOverrides() {
  194. $config = [
  195. 'sourceTable' => $this->author,
  196. 'targetTable' => $this->article,
  197. 'conditions' => ['Articles.is_active' => true],
  198. 'sort' => ['id' => 'ASC'],
  199. 'strategy' => 'select'
  200. ];
  201. $association = new HasMany('Articles', $config);
  202. $keys = [1, 2, 3, 4];
  203. $query = $this->getMock(
  204. 'Cake\ORM\Query',
  205. ['all', 'where', 'andWhere', 'order', 'select', 'contain'],
  206. [null, null]
  207. );
  208. $this->article->expects($this->once())->method('find')->with('all')
  209. ->will($this->returnValue($query));
  210. $results = [
  211. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  212. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  213. ];
  214. $query->expects($this->once())->method('all')
  215. ->will($this->returnValue($results));
  216. $query->expects($this->at(0))->method('where')
  217. ->with(['Articles.is_active' => true])
  218. ->will($this->returnSelf());
  219. $query->expects($this->at(1))->method('where')
  220. ->with(['Articles.id !=' => 3])
  221. ->will($this->returnSelf());
  222. $query->expects($this->once())->method('andWhere')
  223. ->with(['Articles.author_id IN' => $keys])
  224. ->will($this->returnSelf());
  225. $query->expects($this->once())->method('order')
  226. ->with(['title' => 'DESC'])
  227. ->will($this->returnSelf());
  228. $query->expects($this->once())->method('select')
  229. ->with([
  230. 'Articles__title' => 'Articles.title',
  231. 'Articles__author_id' => 'Articles.author_id'
  232. ])
  233. ->will($this->returnSelf());
  234. $query->expects($this->once())->method('contain')
  235. ->with([
  236. 'Categories' => ['fields' => ['a', 'b']],
  237. ])
  238. ->will($this->returnSelf());
  239. $association->eagerLoader([
  240. 'conditions' => ['Articles.id !=' => 3],
  241. 'sort' => ['title' => 'DESC'],
  242. 'fields' => ['title', 'author_id'],
  243. 'contain' => ['Categories' => ['fields' => ['a', 'b']]],
  244. 'keys' => $keys,
  245. 'query' => $query
  246. ]);
  247. }
  248. /**
  249. * Test that failing to add the foreignKey to the list of fields will throw an
  250. * exception
  251. *
  252. * @expectedException \InvalidArgumentException
  253. * @expectedExceptionMessage You are required to select the "Articles.author_id"
  254. * @return void
  255. */
  256. public function testEagerLoaderFieldsException() {
  257. $config = [
  258. 'sourceTable' => $this->author,
  259. 'targetTable' => $this->article,
  260. 'strategy' => 'select'
  261. ];
  262. $association = new HasMany('Articles', $config);
  263. $keys = [1, 2, 3, 4];
  264. $query = $this->getMock(
  265. 'Cake\ORM\Query',
  266. ['all'],
  267. [null, null]
  268. );
  269. $this->article->expects($this->once())->method('find')->with('all')
  270. ->will($this->returnValue($query));
  271. $association->eagerLoader([
  272. 'fields' => ['id', 'title'],
  273. 'keys' => $keys,
  274. 'query' => $query
  275. ]);
  276. }
  277. /**
  278. * Tests eager loading using subquery
  279. *
  280. * @return void
  281. */
  282. public function testEagerLoaderSubquery() {
  283. $config = [
  284. 'sourceTable' => $this->author,
  285. 'targetTable' => $this->article,
  286. ];
  287. $association = new HasMany('Articles', $config);
  288. $parent = (new Query(null, $this->author))
  289. ->join(['foo' => ['table' => 'foo', 'type' => 'inner', 'conditions' => []]])
  290. ->join(['bar' => ['table' => 'bar', 'type' => 'left', 'conditions' => []]]);
  291. $query = $this->getMock(
  292. 'Cake\ORM\Query',
  293. ['all', 'where', 'andWhere', 'order', 'select', 'contain'],
  294. [null, null]
  295. );
  296. $this->article->expects($this->once())->method('find')->with('all')
  297. ->will($this->returnValue($query));
  298. $results = [
  299. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  300. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  301. ];
  302. $query->expects($this->once())->method('all')
  303. ->will($this->returnValue($results));
  304. $query->expects($this->at(0))->method('where')
  305. ->with([])
  306. ->will($this->returnSelf());
  307. $query->expects($this->at(1))->method('where')
  308. ->with([])
  309. ->will($this->returnSelf());
  310. $expected = clone $parent;
  311. $joins = $expected->join();
  312. unset($joins['bar']);
  313. $expected
  314. ->contain([], true)
  315. ->select(['Authors__id' => 'Authors.id'], true)
  316. ->join($joins, [], true);
  317. $query->expects($this->once())->method('andWhere')
  318. ->with(['Articles.author_id IN' => $expected])
  319. ->will($this->returnSelf());
  320. $callable = $association->eagerLoader([
  321. 'query' => $parent, 'strategy' => HasMany::STRATEGY_SUBQUERY, 'keys' => []
  322. ]);
  323. $row = ['Authors__id' => 1, 'username' => 'author 1'];
  324. $result = $callable($row);
  325. $row['Articles'] = [
  326. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  327. ];
  328. $this->assertEquals($row, $result);
  329. $row = ['Authors__id' => 2, 'username' => 'author 2'];
  330. $result = $callable($row);
  331. $row['Articles'] = [
  332. ['id' => 1, 'title' => 'article 1', 'author_id' => 2]
  333. ];
  334. $this->assertEquals($row, $result);
  335. }
  336. /**
  337. * Tests that eager loader accepts a queryBuilder option
  338. *
  339. * @return void
  340. */
  341. public function testEagerLoaderWithQueryBuilder() {
  342. $config = [
  343. 'sourceTable' => $this->author,
  344. 'targetTable' => $this->article,
  345. 'strategy' => 'select'
  346. ];
  347. $association = new HasMany('Articles', $config);
  348. $keys = [1, 2, 3, 4];
  349. $query = $this->getMock(
  350. 'Cake\ORM\Query',
  351. ['all', 'select', 'join', 'where'],
  352. [null, null]
  353. );
  354. $this->article->expects($this->once())->method('find')->with('all')
  355. ->will($this->returnValue($query));
  356. $results = [
  357. ['id' => 1, 'title' => 'article 1', 'author_id' => 2],
  358. ['id' => 2, 'title' => 'article 2', 'author_id' => 1]
  359. ];
  360. $query->expects($this->once())->method('all')
  361. ->will($this->returnValue($results));
  362. $query->expects($this->any())->method('select')
  363. ->will($this->returnSelf());
  364. $query->expects($this->at(2))->method('select')
  365. ->with(['a', 'b'])
  366. ->will($this->returnSelf());
  367. $query->expects($this->at(3))->method('join')
  368. ->with('foo')
  369. ->will($this->returnSelf());
  370. $query->expects($this->any())->method('where')
  371. ->will($this->returnSelf());
  372. $query->expects($this->at(4))->method('where')
  373. ->with(['a' => 1])
  374. ->will($this->returnSelf());
  375. $queryBuilder = function($query) {
  376. return $query->select(['a', 'b'])->join('foo')->where(['a' => 1]);
  377. };
  378. $association->eagerLoader(compact('keys', 'query', 'queryBuilder'));
  379. }
  380. /**
  381. * Test the eager loader method with no extra options
  382. *
  383. * @return void
  384. */
  385. public function testEagerLoaderMultipleKeys() {
  386. $config = [
  387. 'sourceTable' => $this->author,
  388. 'targetTable' => $this->article,
  389. 'strategy' => 'select',
  390. 'foreignKey' => ['author_id', 'site_id']
  391. ];
  392. $this->author->primaryKey(['id', 'site_id']);
  393. $association = new HasMany('Articles', $config);
  394. $keys = [[1, 10], [2, 20], [3, 30], [4, 40]];
  395. $query = $this->getMock('Cake\ORM\Query', ['all', 'andWhere'], [null, null]);
  396. $this->article->expects($this->once())->method('find')->with('all')
  397. ->will($this->returnValue($query));
  398. $results = [
  399. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10],
  400. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20]
  401. ];
  402. $query->expects($this->once())->method('all')
  403. ->will($this->returnValue($results));
  404. $tuple = new TupleComparison(
  405. ['Articles.author_id', 'Articles.site_id'], $keys, [], 'IN'
  406. );
  407. $query->expects($this->once())->method('andWhere')
  408. ->with($tuple)
  409. ->will($this->returnSelf());
  410. $callable = $association->eagerLoader(compact('keys', 'query'));
  411. $row = ['Authors__id' => 2, 'Authors__site_id' => 10, 'username' => 'author 1'];
  412. $result = $callable($row);
  413. $row['Articles'] = [
  414. ['id' => 1, 'title' => 'article 1', 'author_id' => 2, 'site_id' => 10]
  415. ];
  416. $this->assertEquals($row, $result);
  417. $row = ['Authors__id' => 1, 'username' => 'author 2', 'Authors__site_id' => 20];
  418. $result = $callable($row);
  419. $row['Articles'] = [
  420. ['id' => 2, 'title' => 'article 2', 'author_id' => 1, 'site_id' => 20]
  421. ];
  422. $this->assertEquals($row, $result);
  423. }
  424. /**
  425. * Tests that the correct join and fields are attached to a query depending on
  426. * the association config
  427. *
  428. * @return void
  429. */
  430. public function testAttachTo() {
  431. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  432. $config = [
  433. 'sourceTable' => $this->author,
  434. 'targetTable' => $this->article,
  435. 'conditions' => ['Articles.is_active' => true]
  436. ];
  437. $field = new IdentifierExpression('Articles.author_id');
  438. $association = new HasMany('Articles', $config);
  439. $query->expects($this->once())->method('join')->with([
  440. 'Articles' => [
  441. 'conditions' => new QueryExpression([
  442. 'Articles.is_active' => true,
  443. ['Authors.id' => $field]
  444. ], $this->articlesTypeMap),
  445. 'type' => 'INNER',
  446. 'table' => 'articles'
  447. ]
  448. ]);
  449. $query->expects($this->once())->method('select')->with([
  450. 'Articles__id' => 'Articles.id',
  451. 'Articles__title' => 'Articles.title',
  452. 'Articles__author_id' => 'Articles.author_id'
  453. ]);
  454. $association->attachTo($query);
  455. }
  456. /**
  457. * Tests that default config defined in the association can be overridden
  458. *
  459. * @return void
  460. */
  461. public function testAttachToConfigOverride() {
  462. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  463. $config = [
  464. 'sourceTable' => $this->author,
  465. 'targetTable' => $this->article,
  466. 'conditions' => ['Articles.is_active' => true]
  467. ];
  468. $association = new HasMany('Articles', $config);
  469. $query->expects($this->once())->method('join')->with([
  470. 'Articles' => [
  471. 'conditions' => new QueryExpression([
  472. 'Articles.is_active' => false
  473. ], $this->articlesTypeMap),
  474. 'type' => 'INNER',
  475. 'table' => 'articles'
  476. ]
  477. ]);
  478. $query->expects($this->once())->method('select')->with([
  479. 'Articles__title' => 'Articles.title'
  480. ]);
  481. $override = [
  482. 'conditions' => ['Articles.is_active' => false],
  483. 'foreignKey' => false,
  484. 'fields' => ['title']
  485. ];
  486. $association->attachTo($query, $override);
  487. }
  488. /**
  489. * Tests that it is possible to avoid fields inclusion for the associated table
  490. *
  491. * @return void
  492. */
  493. public function testAttachToNoFields() {
  494. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  495. $config = [
  496. 'sourceTable' => $this->author,
  497. 'targetTable' => $this->article,
  498. 'conditions' => ['Articles.is_active' => true]
  499. ];
  500. $field = new IdentifierExpression('Articles.author_id');
  501. $association = new HasMany('Articles', $config);
  502. $query->expects($this->once())->method('join')->with([
  503. 'Articles' => [
  504. 'conditions' => new QueryExpression([
  505. 'Articles.is_active' => true,
  506. ['Authors.id' => $field]
  507. ], $this->articlesTypeMap),
  508. 'type' => 'INNER',
  509. 'table' => 'articles'
  510. ]
  511. ]);
  512. $query->expects($this->never())->method('select');
  513. $association->attachTo($query, ['includeFields' => false]);
  514. }
  515. /**
  516. * Tests that using hasMany with a table having a multi column primary
  517. * key will work if the foreign key is passed
  518. *
  519. * @return void
  520. */
  521. public function testAttachToMultiPrimaryKey() {
  522. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  523. $this->author->primaryKey(['id', 'site_id']);
  524. $config = [
  525. 'sourceTable' => $this->author,
  526. 'targetTable' => $this->article,
  527. 'conditions' => ['Articles.is_active' => true],
  528. 'foreignKey' => ['author_id', 'author_site_id']
  529. ];
  530. $field1 = new IdentifierExpression('Articles.author_id');
  531. $field2 = new IdentifierExpression('Articles.author_site_id');
  532. $association = new HasMany('Articles', $config);
  533. $query->expects($this->once())->method('join')->with([
  534. 'Articles' => [
  535. 'conditions' => new QueryExpression([
  536. 'Articles.is_active' => true,
  537. ['Authors.id' => $field1, 'Authors.site_id' => $field2]
  538. ], $this->articlesTypeMap),
  539. 'type' => 'INNER',
  540. 'table' => 'articles'
  541. ]
  542. ]);
  543. $query->expects($this->never())->method('select');
  544. $association->attachTo($query, ['includeFields' => false]);
  545. }
  546. /**
  547. * Tests that using hasMany with a table having a multi column primary
  548. * key will work if the foreign key is passed
  549. *
  550. * @expectedException \RuntimeException
  551. * @expectedExceptionMessage Cannot match provided foreignKey for "Articles", got "(author_id)" but expected foreign key for "(id, site_id)
  552. * @return void
  553. */
  554. public function testAttachToMultiPrimaryKeyMistmatch() {
  555. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  556. $this->author->primaryKey(['id', 'site_id']);
  557. $config = [
  558. 'sourceTable' => $this->author,
  559. 'targetTable' => $this->article,
  560. 'conditions' => ['Articles.is_active' => true],
  561. 'foreignKey' => 'author_id'
  562. ];
  563. $field1 = new IdentifierExpression('Articles.author_id');
  564. $field2 = new IdentifierExpression('Articles.author_site_id');
  565. $association = new HasMany('Articles', $config);
  566. $association->attachTo($query, ['includeFields' => false]);
  567. }
  568. /**
  569. * Tests that by supplying a query builder function, it is possible to add fields
  570. * and conditions to an association
  571. *
  572. * @return void
  573. */
  574. public function testAttachToWithQueryBuilder() {
  575. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  576. $config = [
  577. 'sourceTable' => $this->author,
  578. 'targetTable' => $this->article,
  579. 'conditions' => ['Articles.is_active' => true]
  580. ];
  581. $field = new IdentifierExpression('Articles.author_id');
  582. $association = new HasMany('Articles', $config);
  583. $query->expects($this->once())->method('join')->with([
  584. 'Articles' => [
  585. 'conditions' => new QueryExpression([
  586. 'a' => 1,
  587. 'Articles.is_active' => true,
  588. ['Authors.id' => $field],
  589. ], $this->articlesTypeMap),
  590. 'type' => 'INNER',
  591. 'table' => 'articles'
  592. ]
  593. ]);
  594. $query->expects($this->once())->method('select')
  595. ->with([
  596. 'Articles__a' => 'Articles.a',
  597. 'Articles__b' => 'Articles.b'
  598. ]);
  599. $builder = function($q) {
  600. return $q->select(['a', 'b'])->where(['a' => 1]);
  601. };
  602. $association->attachTo($query, ['queryBuilder' => $builder]);
  603. }
  604. /**
  605. * Test cascading deletes.
  606. *
  607. * @return void
  608. */
  609. public function testCascadeDelete() {
  610. $config = [
  611. 'dependent' => true,
  612. 'sourceTable' => $this->author,
  613. 'targetTable' => $this->article,
  614. 'conditions' => ['Articles.is_active' => true],
  615. ];
  616. $association = new HasMany('Articles', $config);
  617. $this->article->expects($this->once())
  618. ->method('deleteAll')
  619. ->with([
  620. 'Articles.is_active' => true,
  621. 'author_id' => 1
  622. ]);
  623. $entity = new Entity(['id' => 1, 'name' => 'PHP']);
  624. $association->cascadeDelete($entity);
  625. }
  626. /**
  627. * Test cascading delete with has many.
  628. *
  629. * @return void
  630. */
  631. public function testCascadeDeleteCallbacks() {
  632. $config = [
  633. 'dependent' => true,
  634. 'sourceTable' => $this->author,
  635. 'targetTable' => $this->article,
  636. 'conditions' => ['Articles.is_active' => true],
  637. 'cascadeCallbacks' => true,
  638. ];
  639. $association = new HasMany('Articles', $config);
  640. $articleOne = new Entity(['id' => 2, 'title' => 'test']);
  641. $articleTwo = new Entity(['id' => 3, 'title' => 'testing']);
  642. $iterator = new \ArrayIterator([
  643. $articleOne,
  644. $articleTwo
  645. ]);
  646. $query = $this->getMock('\Cake\ORM\Query', [], [], '', false);
  647. $query->expects($this->at(0))
  648. ->method('where')
  649. ->with(['Articles.is_active' => true])
  650. ->will($this->returnSelf());
  651. $query->expects($this->at(1))
  652. ->method('where')
  653. ->with(['author_id' => 1])
  654. ->will($this->returnSelf());
  655. $query->expects($this->any())
  656. ->method('getIterator')
  657. ->will($this->returnValue($iterator));
  658. $query->expects($this->once())
  659. ->method('bufferResults')
  660. ->with(false)
  661. ->will($this->returnSelf());
  662. $this->article->expects($this->once())
  663. ->method('find')
  664. ->will($this->returnValue($query));
  665. $this->article->expects($this->at(1))
  666. ->method('delete')
  667. ->with($articleOne, []);
  668. $this->article->expects($this->at(2))
  669. ->method('delete')
  670. ->with($articleTwo, []);
  671. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  672. $this->assertTrue($association->cascadeDelete($entity));
  673. }
  674. /**
  675. * Test that save() ignores non entity values.
  676. *
  677. * @return void
  678. */
  679. public function testSaveOnlyEntities() {
  680. $mock = $this->getMock('Cake\ORM\Table', [], [], '', false);
  681. $config = [
  682. 'sourceTable' => $this->author,
  683. 'targetTable' => $mock,
  684. ];
  685. $entity = new Entity([
  686. 'username' => 'Mark',
  687. 'email' => 'mark@example.com',
  688. 'articles' => [
  689. ['title' => 'First Post'],
  690. new Entity(['title' => 'Second Post']),
  691. ]
  692. ]);
  693. $mock->expects($this->never())
  694. ->method('save');
  695. $association = new HasMany('Articles', $config);
  696. $association->save($entity);
  697. }
  698. /**
  699. * Tests that property is being set using the constructor options.
  700. *
  701. * @return void
  702. */
  703. public function testPropertyOption() {
  704. $config = ['propertyName' => 'thing_placeholder'];
  705. $association = new hasMany('Thing', $config);
  706. $this->assertEquals('thing_placeholder', $association->property());
  707. }
  708. /**
  709. * Test that plugin names are omitted from property()
  710. *
  711. * @return void
  712. */
  713. public function testPropertyNoPlugin() {
  714. $mock = $this->getMock('Cake\ORM\Table', [], [], '', false);
  715. $config = [
  716. 'sourceTable' => $this->author,
  717. 'targetTable' => $mock,
  718. ];
  719. $association = new HasMany('Contacts.Addresses', $config);
  720. $this->assertEquals('addresses', $association->property());
  721. }
  722. /**
  723. * Tests that attaching an association to a query will trigger beforeFind
  724. * for the target table
  725. *
  726. * @return void
  727. */
  728. public function testAttachToBeforeFind() {
  729. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  730. $config = [
  731. 'sourceTable' => $this->author,
  732. 'targetTable' => $this->article,
  733. ];
  734. $listener = $this->getMock('stdClass', ['__invoke']);
  735. $association = new HasMany('Articles', $config);
  736. $this->article->getEventManager()->attach($listener, 'Model.beforeFind');
  737. $listener->expects($this->once())->method('__invoke')
  738. ->with(
  739. $this->isInstanceOf('\Cake\Event\Event'),
  740. $this->isInstanceOf('\Cake\ORM\Query'),
  741. [],
  742. false
  743. );
  744. $association->attachTo($query);
  745. }
  746. /**
  747. * Tests that attaching an association to a query will trigger beforeFind
  748. * for the target table
  749. *
  750. * @return void
  751. */
  752. public function testAttachToBeforeFindExtraOptions() {
  753. $query = $this->getMock('\Cake\ORM\Query', ['join', 'select'], [null, null]);
  754. $config = [
  755. 'sourceTable' => $this->author,
  756. 'targetTable' => $this->article,
  757. ];
  758. $listener = $this->getMock('stdClass', ['__invoke']);
  759. $association = new HasMany('Articles', $config);
  760. $this->article->getEventManager()->attach($listener, 'Model.beforeFind');
  761. $opts = ['something' => 'more'];
  762. $listener->expects($this->once())->method('__invoke')
  763. ->with(
  764. $this->isInstanceOf('\Cake\Event\Event'),
  765. $this->isInstanceOf('\Cake\ORM\Query'),
  766. $opts,
  767. false
  768. );
  769. $association->attachTo($query, ['queryBuilder' => function($q) {
  770. return $q->applyOptions(['something' => 'more']);
  771. }]);
  772. }
  773. }