EagerLoaderTest.php 22 KB

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