PageRenderTime 60ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/TestCase/ORM/Association/HasManyTest.php

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