EagerLoaderTest.php 21 KB

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