EagerLoaderTest.php 17 KB

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