EagerLoaderTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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;
  16. use Cake\Database\Expression\IdentifierExpression;
  17. use Cake\Database\Expression\QueryExpression;
  18. use Cake\Database\TypeMap;
  19. use Cake\Datasource\ConnectionManager;
  20. use Cake\ORM\EagerLoader;
  21. use Cake\ORM\Query;
  22. use Cake\ORM\TableRegistry;
  23. use Cake\TestSuite\TestCase;
  24. use InvalidArgumentException;
  25. /**
  26. * Tests EagerLoader
  27. */
  28. class EagerLoaderTest extends TestCase
  29. {
  30. /**
  31. * setUp method
  32. *
  33. * @return void
  34. */
  35. public function setUp()
  36. {
  37. parent::setUp();
  38. $this->connection = ConnectionManager::get('test');
  39. $schema = [
  40. 'id' => ['type' => 'integer'],
  41. '_constraints' => [
  42. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  43. ]
  44. ];
  45. $schema1 = [
  46. 'id' => ['type' => 'integer'],
  47. 'name' => ['type' => 'string'],
  48. 'phone' => ['type' => 'string'],
  49. '_constraints' => [
  50. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  51. ]
  52. ];
  53. $schema2 = [
  54. 'id' => ['type' => 'integer'],
  55. 'total' => ['type' => 'string'],
  56. 'placed' => ['type' => 'datetime'],
  57. '_constraints' => [
  58. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  59. ]
  60. ];
  61. $this->table = $table = TableRegistry::get('foo', ['schema' => $schema]);
  62. $clients = TableRegistry::get('clients', ['schema' => $schema1]);
  63. $orders = TableRegistry::get('orders', ['schema' => $schema2]);
  64. $companies = TableRegistry::get('companies', ['schema' => $schema, 'table' => 'organizations']);
  65. $orderTypes = TableRegistry::get('orderTypes', ['schema' => $schema]);
  66. $stuff = TableRegistry::get('stuff', ['schema' => $schema, 'table' => 'things']);
  67. $stuffTypes = TableRegistry::get('stuffTypes', ['schema' => $schema]);
  68. $categories = TableRegistry::get('categories', ['schema' => $schema]);
  69. $table->belongsTo('clients');
  70. $clients->hasOne('orders');
  71. $clients->belongsTo('companies');
  72. $orders->belongsTo('orderTypes');
  73. $orders->hasOne('stuff');
  74. $stuff->belongsTo('stuffTypes');
  75. $companies->belongsTo('categories');
  76. $this->clientsTypeMap = new TypeMap([
  77. 'clients.id' => 'integer',
  78. 'id' => 'integer',
  79. 'clients.name' => 'string',
  80. 'name' => 'string',
  81. 'clients.phone' => 'string',
  82. 'phone' => 'string',
  83. 'clients__id' => 'integer',
  84. 'clients__name' => 'string',
  85. 'clients__phone' => 'string',
  86. ]);
  87. $this->ordersTypeMap = new TypeMap([
  88. 'orders.id' => 'integer',
  89. 'id' => 'integer',
  90. 'orders.total' => 'string',
  91. 'total' => 'string',
  92. 'orders.placed' => 'datetime',
  93. 'placed' => 'datetime',
  94. 'orders__id' => 'integer',
  95. 'orders__total' => 'string',
  96. 'orders__placed' => 'datetime',
  97. ]);
  98. $this->orderTypesTypeMap = new TypeMap([
  99. 'orderTypes.id' => 'integer',
  100. 'id' => 'integer',
  101. 'orderTypes__id' => 'integer',
  102. ]);
  103. $this->stuffTypeMap = new TypeMap([
  104. 'stuff.id' => 'integer',
  105. 'id' => 'integer',
  106. 'stuff__id' => 'integer',
  107. ]);
  108. $this->stuffTypesTypeMap = new TypeMap([
  109. 'stuffTypes.id' => 'integer',
  110. 'id' => 'integer',
  111. 'stuffTypes__id' => 'integer',
  112. ]);
  113. $this->companiesTypeMap = new TypeMap([
  114. 'companies.id' => 'integer',
  115. 'id' => 'integer',
  116. 'companies__id' => 'integer',
  117. ]);
  118. $this->categoriesTypeMap = new TypeMap([
  119. 'categories.id' => 'integer',
  120. 'id' => 'integer',
  121. 'categories__id' => 'integer',
  122. ]);
  123. }
  124. /**
  125. * tearDown method
  126. *
  127. * @return void
  128. */
  129. public function tearDown()
  130. {
  131. parent::tearDown();
  132. TableRegistry::clear();
  133. }
  134. /**
  135. * Tests that fully defined belongsTo and hasOne relationships are joined correctly
  136. *
  137. * @return void
  138. */
  139. public function testContainToJoinsOneLevel()
  140. {
  141. $contains = [
  142. 'clients' => [
  143. 'orders' => [
  144. 'orderTypes',
  145. 'stuff' => ['stuffTypes']
  146. ],
  147. 'companies' => [
  148. 'foreignKey' => 'organization_id',
  149. 'categories'
  150. ]
  151. ]
  152. ];
  153. $query = $this->getMockBuilder('\Cake\ORM\Query')
  154. ->setMethods(['join'])
  155. ->setConstructorArgs([$this->connection, $this->table])
  156. ->getMock();
  157. $query->typeMap($this->clientsTypeMap);
  158. $query->expects($this->at(0))->method('join')
  159. ->with(['clients' => [
  160. 'table' => 'clients',
  161. 'type' => 'LEFT',
  162. 'conditions' => new QueryExpression([
  163. ['clients.id' => new IdentifierExpression('foo.client_id')],
  164. ], new TypeMap($this->clientsTypeMap->defaults()))
  165. ]])
  166. ->will($this->returnValue($query));
  167. $query->expects($this->at(1))->method('join')
  168. ->with(['orders' => [
  169. 'table' => 'orders',
  170. 'type' => 'LEFT',
  171. 'conditions' => new QueryExpression([
  172. ['clients.id' => new IdentifierExpression('orders.client_id')]
  173. ], $this->ordersTypeMap)
  174. ]])
  175. ->will($this->returnValue($query));
  176. $query->expects($this->at(2))->method('join')
  177. ->with(['orderTypes' => [
  178. 'table' => 'order_types',
  179. 'type' => 'LEFT',
  180. 'conditions' => new QueryExpression([
  181. ['orderTypes.id' => new IdentifierExpression('orders.order_type_id')]
  182. ], $this->orderTypesTypeMap)
  183. ]])
  184. ->will($this->returnValue($query));
  185. $query->expects($this->at(3))->method('join')
  186. ->with(['stuff' => [
  187. 'table' => 'things',
  188. 'type' => 'LEFT',
  189. 'conditions' => new QueryExpression([
  190. ['orders.id' => new IdentifierExpression('stuff.order_id')]
  191. ], $this->stuffTypeMap)
  192. ]])
  193. ->will($this->returnValue($query));
  194. $query->expects($this->at(4))->method('join')
  195. ->with(['stuffTypes' => [
  196. 'table' => 'stuff_types',
  197. 'type' => 'LEFT',
  198. 'conditions' => new QueryExpression([
  199. ['stuffTypes.id' => new IdentifierExpression('stuff.stuff_type_id')]
  200. ], $this->stuffTypesTypeMap)
  201. ]])
  202. ->will($this->returnValue($query));
  203. $query->expects($this->at(5))->method('join')
  204. ->with(['companies' => [
  205. 'table' => 'organizations',
  206. 'type' => 'LEFT',
  207. 'conditions' => new QueryExpression([
  208. ['companies.id' => new IdentifierExpression('clients.organization_id')]
  209. ], $this->companiesTypeMap)
  210. ]])
  211. ->will($this->returnValue($query));
  212. $query->expects($this->at(6))->method('join')
  213. ->with(['categories' => [
  214. 'table' => 'categories',
  215. 'type' => 'LEFT',
  216. 'conditions' => new QueryExpression([
  217. ['categories.id' => new IdentifierExpression('companies.category_id')]
  218. ], $this->categoriesTypeMap)
  219. ]])
  220. ->will($this->returnValue($query));
  221. $loader = new EagerLoader;
  222. $loader->contain($contains);
  223. $query->select('foo.id')->eagerLoader($loader)->sql();
  224. }
  225. /**
  226. * Tests setting containments using dot notation, additionally proves that options
  227. * are not overwritten when combining dot notation and array notation
  228. *
  229. * @return void
  230. */
  231. public function testContainDotNotation()
  232. {
  233. $loader = new EagerLoader;
  234. $loader->contain([
  235. 'clients.orders.stuff',
  236. 'clients.companies.categories' => ['conditions' => ['a >' => 1]]
  237. ]);
  238. $expected = [
  239. 'clients' => [
  240. 'orders' => [
  241. 'stuff' => []
  242. ],
  243. 'companies' => [
  244. 'categories' => [
  245. 'conditions' => ['a >' => 1]
  246. ]
  247. ]
  248. ]
  249. ];
  250. $this->assertEquals($expected, $loader->contain());
  251. $loader->contain([
  252. 'clients.orders' => ['fields' => ['a', 'b']],
  253. 'clients' => ['sort' => ['a' => 'desc']],
  254. ]);
  255. $expected['clients']['orders'] += ['fields' => ['a', 'b']];
  256. $expected['clients'] += ['sort' => ['a' => 'desc']];
  257. $this->assertEquals($expected, $loader->contain());
  258. }
  259. /**
  260. * Tests setting containments using direct key value pairs works just as with key array.
  261. *
  262. * @return void
  263. */
  264. public function testContainKeyValueNotation()
  265. {
  266. $loader = new EagerLoader;
  267. $loader->contain([
  268. 'clients',
  269. 'companies' => 'categories',
  270. ]);
  271. $expected = [
  272. 'clients' => [
  273. ],
  274. 'companies' => [
  275. 'categories' => [
  276. ],
  277. ],
  278. ];
  279. $this->assertEquals($expected, $loader->contain());
  280. }
  281. /**
  282. * Tests that it is possible to pass a function as the array value for contain
  283. *
  284. * @return void
  285. */
  286. public function testContainClosure()
  287. {
  288. $builder = function ($query) {
  289. };
  290. $loader = new EagerLoader;
  291. $loader->contain([
  292. 'clients.orders.stuff' => ['fields' => ['a']],
  293. 'clients' => $builder
  294. ]);
  295. $expected = [
  296. 'clients' => [
  297. 'orders' => [
  298. 'stuff' => ['fields' => ['a']]
  299. ],
  300. 'queryBuilder' => $builder
  301. ]
  302. ];
  303. $this->assertEquals($expected, $loader->contain());
  304. $loader = new EagerLoader;
  305. $loader->contain([
  306. 'clients.orders.stuff' => ['fields' => ['a']],
  307. 'clients' => ['queryBuilder' => $builder]
  308. ]);
  309. $this->assertEquals($expected, $loader->contain());
  310. }
  311. /**
  312. * Tests using the same signature as matching with contain
  313. *
  314. * @return void
  315. */
  316. public function testContainSecondSignature()
  317. {
  318. $builder = function ($query) {
  319. };
  320. $loader = new EagerLoader;
  321. $loader->contain('clients', $builder);
  322. $expected = [
  323. 'clients' => [
  324. 'queryBuilder' => $builder
  325. ]
  326. ];
  327. $this->assertEquals($expected, $loader->contain());
  328. }
  329. /**
  330. * Tests passing an array of associations with a query builder
  331. *
  332. * @return void
  333. */
  334. public function testContainSecondSignatureInvalid()
  335. {
  336. $this->expectException(InvalidArgumentException::class);
  337. $builder = function ($query) {
  338. };
  339. $loader = new EagerLoader;
  340. $loader->contain(['clients'], $builder);
  341. $expected = [
  342. 'clients' => [
  343. 'queryBuilder' => $builder
  344. ]
  345. ];
  346. $this->assertEquals($expected, $loader->contain());
  347. }
  348. /**
  349. * Tests that query builders are stacked
  350. *
  351. * @return void
  352. */
  353. public function testContainMergeBuilders()
  354. {
  355. $loader = new EagerLoader;
  356. $loader->contain([
  357. 'clients' => function ($query) {
  358. return $query->select(['a']);
  359. }
  360. ]);
  361. $loader->contain([
  362. 'clients' => function ($query) {
  363. return $query->select(['b']);
  364. }
  365. ]);
  366. $builder = $loader->contain()['clients']['queryBuilder'];
  367. $table = TableRegistry::get('foo');
  368. $query = new Query($this->connection, $table);
  369. $query = $builder($query);
  370. $this->assertEquals(['a', 'b'], $query->clause('select'));
  371. }
  372. /**
  373. * Test that fields for contained models are aliased and added to the select clause
  374. *
  375. * @return void
  376. */
  377. public function testContainToFieldsPredefined()
  378. {
  379. $contains = [
  380. 'clients' => [
  381. 'fields' => ['name', 'company_id', 'clients.telephone'],
  382. 'orders' => [
  383. 'fields' => ['total', 'placed']
  384. ]
  385. ]
  386. ];
  387. $table = TableRegistry::get('foo');
  388. $query = new Query($this->connection, $table);
  389. $loader = new EagerLoader;
  390. $loader->contain($contains);
  391. $query->select('foo.id');
  392. $loader->attachAssociations($query, $table, true);
  393. $select = $query->clause('select');
  394. $expected = [
  395. 'foo.id', 'clients__name' => 'clients.name',
  396. 'clients__company_id' => 'clients.company_id',
  397. 'clients__telephone' => 'clients.telephone',
  398. 'orders__total' => 'orders.total', 'orders__placed' => 'orders.placed'
  399. ];
  400. $this->assertEquals($expected, $select);
  401. }
  402. /**
  403. * Tests that default fields for associations are added to the select clause when
  404. * none is specified
  405. *
  406. * @return void
  407. */
  408. public function testContainToFieldsDefault()
  409. {
  410. $contains = ['clients' => ['orders']];
  411. $query = new Query($this->connection, $this->table);
  412. $query->select()->contain($contains)->sql();
  413. $select = $query->clause('select');
  414. $expected = [
  415. 'foo__id' => 'foo.id', 'clients__name' => 'clients.name',
  416. 'clients__id' => 'clients.id', 'clients__phone' => 'clients.phone',
  417. 'orders__id' => 'orders.id', 'orders__total' => 'orders.total',
  418. 'orders__placed' => 'orders.placed'
  419. ];
  420. $expected = $this->_quoteArray($expected);
  421. $this->assertEquals($expected, $select);
  422. $contains['clients']['fields'] = ['name'];
  423. $query = new Query($this->connection, $this->table);
  424. $query->select('foo.id')->contain($contains)->sql();
  425. $select = $query->clause('select');
  426. $expected = ['foo__id' => 'foo.id', 'clients__name' => 'clients.name'];
  427. $expected = $this->_quoteArray($expected);
  428. $this->assertEquals($expected, $select);
  429. $contains['clients']['fields'] = [];
  430. $contains['clients']['orders']['fields'] = false;
  431. $query = new Query($this->connection, $this->table);
  432. $query->select()->contain($contains)->sql();
  433. $select = $query->clause('select');
  434. $expected = [
  435. 'foo__id' => 'foo.id',
  436. 'clients__id' => 'clients.id',
  437. 'clients__name' => 'clients.name',
  438. 'clients__phone' => 'clients.phone',
  439. ];
  440. $expected = $this->_quoteArray($expected);
  441. $this->assertEquals($expected, $select);
  442. }
  443. /**
  444. * Tests that the path for getting to a deep association is materialized in an
  445. * array key
  446. *
  447. * @return void
  448. */
  449. public function testNormalizedPath()
  450. {
  451. $contains = [
  452. 'clients' => [
  453. 'orders' => [
  454. 'orderTypes',
  455. 'stuff' => ['stuffTypes']
  456. ],
  457. 'companies' => [
  458. 'categories'
  459. ]
  460. ]
  461. ];
  462. $query = $this->getMockBuilder('\Cake\ORM\Query')
  463. ->setMethods(['join'])
  464. ->setConstructorArgs([$this->connection, $this->table])
  465. ->getMock();
  466. $loader = new EagerLoader;
  467. $loader->contain($contains);
  468. $normalized = $loader->normalized($this->table);
  469. $this->assertEquals('clients', $normalized['clients']->aliasPath());
  470. $this->assertEquals('client', $normalized['clients']->propertyPath());
  471. $assocs = $normalized['clients']->associations();
  472. $this->assertEquals('clients.orders', $assocs['orders']->aliasPath());
  473. $this->assertEquals('client.order', $assocs['orders']->propertyPath());
  474. $assocs = $assocs['orders']->associations();
  475. $this->assertEquals('clients.orders.orderTypes', $assocs['orderTypes']->aliasPath());
  476. $this->assertEquals('client.order.order_type', $assocs['orderTypes']->propertyPath());
  477. $this->assertEquals('clients.orders.stuff', $assocs['stuff']->aliasPath());
  478. $this->assertEquals('client.order.stuff', $assocs['stuff']->propertyPath());
  479. $assocs = $assocs['stuff']->associations();
  480. $this->assertEquals(
  481. 'clients.orders.stuff.stuffTypes',
  482. $assocs['stuffTypes']->aliasPath()
  483. );
  484. $this->assertEquals(
  485. 'client.order.stuff.stuff_type',
  486. $assocs['stuffTypes']->propertyPath()
  487. );
  488. }
  489. /**
  490. * Test clearing containments but not matching joins.
  491. *
  492. * @return void
  493. */
  494. public function testClearContain()
  495. {
  496. $contains = [
  497. 'clients' => [
  498. 'orders' => [
  499. 'orderTypes',
  500. 'stuff' => ['stuffTypes']
  501. ],
  502. 'companies' => [
  503. 'categories'
  504. ]
  505. ]
  506. ];
  507. $loader = new EagerLoader();
  508. $loader->contain($contains);
  509. $loader->matching('clients.addresses');
  510. $this->assertNull($loader->clearContain());
  511. $result = $loader->normalized($this->table);
  512. $this->assertEquals([], $result);
  513. $this->assertArrayHasKey('clients', $loader->matching());
  514. }
  515. /**
  516. * Helper function sued to quoted both keys and values in an array in case
  517. * the test suite is running with auto quoting enabled
  518. *
  519. * @param array $elements
  520. * @return array
  521. */
  522. protected function _quoteArray($elements)
  523. {
  524. if ($this->connection->driver()->autoQuoting()) {
  525. $quoter = function ($e) {
  526. return $this->connection->driver()->quoteIdentifier($e);
  527. };
  528. return array_combine(
  529. array_map($quoter, array_keys($elements)),
  530. array_map($quoter, array_values($elements))
  531. );
  532. }
  533. return $elements;
  534. }
  535. /**
  536. * Asserts that matching('something') and setMatching('something') return consistent type.
  537. *
  538. * @return void
  539. */
  540. public function testSetMatchingReturnType()
  541. {
  542. $loader = new EagerLoader();
  543. $result = $loader->setMatching('clients');
  544. $this->assertInstanceOf(EagerLoader::class, $result);
  545. $this->assertArrayHasKey('clients', $loader->getMatching());
  546. $result = $loader->matching('customers');
  547. $this->assertArrayHasKey('customers', $result);
  548. $this->assertArrayHasKey('customers', $loader->getMatching());
  549. }
  550. }