EagerLoaderTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. * setUp method
  32. *
  33. * @return void
  34. */
  35. public function setUp() {
  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 = TableRegistry::get('foo', ['schema' => $schema]);
  61. $clients = TableRegistry::get('clients', ['schema' => $schema1]);
  62. $orders = TableRegistry::get('orders', ['schema' => $schema2]);
  63. $companies = TableRegistry::get('companies', ['schema' => $schema, 'table' => 'organizations']);
  64. $orderTypes = TableRegistry::get('orderTypes', ['schema' => $schema]);
  65. $stuff = TableRegistry::get('stuff', ['schema' => $schema, 'table' => 'things']);
  66. $stuffTypes = TableRegistry::get('stuffTypes', ['schema' => $schema]);
  67. $categories = TableRegistry::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. ]);
  83. $this->ordersTypeMap = new TypeMap([
  84. 'orders.id' => 'integer',
  85. 'id' => 'integer',
  86. 'orders.total' => 'string',
  87. 'total' => 'string',
  88. 'orders.placed' => 'datetime',
  89. 'placed' => 'datetime',
  90. ]);
  91. $this->orderTypesTypeMap = new TypeMap([
  92. 'orderTypes.id' => 'integer',
  93. 'id' => 'integer',
  94. ]);
  95. $this->stuffTypeMap = new TypeMap([
  96. 'stuff.id' => 'integer',
  97. 'id' => 'integer',
  98. ]);
  99. $this->stuffTypesTypeMap = new TypeMap([
  100. 'stuffTypes.id' => 'integer',
  101. 'id' => 'integer',
  102. ]);
  103. $this->companiesTypeMap = new TypeMap([
  104. 'companies.id' => 'integer',
  105. 'id' => 'integer',
  106. ]);
  107. $this->categoriesTypeMap = new TypeMap([
  108. 'categories.id' => 'integer',
  109. 'id' => 'integer',
  110. ]);
  111. }
  112. /**
  113. * tearDown method
  114. *
  115. * @return void
  116. */
  117. public function tearDown() {
  118. parent::tearDown();
  119. TableRegistry::clear();
  120. }
  121. /**
  122. * Tests that fully defined belongsTo and hasOne relationships are joined correctly
  123. *
  124. * @return void
  125. */
  126. public function testContainToJoinsOneLevel() {
  127. $contains = [
  128. 'clients' => [
  129. 'orders' => [
  130. 'orderTypes',
  131. 'stuff' => ['stuffTypes']
  132. ],
  133. 'companies' => [
  134. 'foreignKey' => 'organization_id',
  135. 'categories'
  136. ]
  137. ]
  138. ];
  139. $query = $this->getMock('\Cake\ORM\Query', ['join'], [$this->connection, $this->table]);
  140. $query->typeMap($this->clientsTypeMap);
  141. $query->expects($this->at(0))->method('join')
  142. ->with(['clients' => [
  143. 'table' => 'clients',
  144. 'type' => 'LEFT',
  145. 'conditions' => new QueryExpression([
  146. ['clients.id' => new IdentifierExpression('foo.client_id')],
  147. ], $this->clientsTypeMap)
  148. ]])
  149. ->will($this->returnValue($query));
  150. $query->expects($this->at(1))->method('join')
  151. ->with(['orders' => [
  152. 'table' => 'orders',
  153. 'type' => 'LEFT',
  154. 'conditions' => new QueryExpression([
  155. ['clients.id' => new IdentifierExpression('orders.client_id')]
  156. ], $this->ordersTypeMap)
  157. ]])
  158. ->will($this->returnValue($query));
  159. $query->expects($this->at(2))->method('join')
  160. ->with(['orderTypes' => [
  161. 'table' => 'order_types',
  162. 'type' => 'LEFT',
  163. 'conditions' => new QueryExpression([
  164. ['orderTypes.id' => new IdentifierExpression('orders.order_type_id')]
  165. ], $this->orderTypesTypeMap)
  166. ]])
  167. ->will($this->returnValue($query));
  168. $query->expects($this->at(3))->method('join')
  169. ->with(['stuff' => [
  170. 'table' => 'things',
  171. 'type' => 'LEFT',
  172. 'conditions' => new QueryExpression([
  173. ['orders.id' => new IdentifierExpression('stuff.order_id')]
  174. ], $this->stuffTypeMap)
  175. ]])
  176. ->will($this->returnValue($query));
  177. $query->expects($this->at(4))->method('join')
  178. ->with(['stuffTypes' => [
  179. 'table' => 'stuff_types',
  180. 'type' => 'LEFT',
  181. 'conditions' => new QueryExpression([
  182. ['stuffTypes.id' => new IdentifierExpression('stuff.stuff_type_id')]
  183. ], $this->stuffTypesTypeMap)
  184. ]])
  185. ->will($this->returnValue($query));
  186. $query->expects($this->at(5))->method('join')
  187. ->with(['companies' => [
  188. 'table' => 'organizations',
  189. 'type' => 'LEFT',
  190. 'conditions' => new QueryExpression([
  191. ['companies.id' => new IdentifierExpression('clients.organization_id')]
  192. ], $this->companiesTypeMap)
  193. ]])
  194. ->will($this->returnValue($query));
  195. $query->expects($this->at(6))->method('join')
  196. ->with(['categories' => [
  197. 'table' => 'categories',
  198. 'type' => 'LEFT',
  199. 'conditions' => new QueryExpression([
  200. ['categories.id' => new IdentifierExpression('companies.category_id')]
  201. ], $this->categoriesTypeMap)
  202. ]])
  203. ->will($this->returnValue($query));
  204. $loader = new EagerLoader;
  205. $loader->contain($contains);
  206. $query->select('foo.id')->eagerLoader($loader)->sql();
  207. }
  208. /**
  209. * Tests setting containments using dot notation, additionally proves that options
  210. * are not overwritten when combining dot notation and array notation
  211. *
  212. * @return void
  213. */
  214. public function testContainDotNotation() {
  215. $loader = new EagerLoader;
  216. $loader->contain([
  217. 'clients.orders.stuff',
  218. 'clients.companies.categories' => ['conditions' => ['a >' => 1]]
  219. ]);
  220. $expected = [
  221. 'clients' => [
  222. 'orders' => [
  223. 'stuff' => []
  224. ],
  225. 'companies' => [
  226. 'categories' => [
  227. 'conditions' => ['a >' => 1]
  228. ]
  229. ]
  230. ]
  231. ];
  232. $this->assertEquals($expected, $loader->contain());
  233. $loader->contain([
  234. 'clients.orders' => ['fields' => ['a', 'b']],
  235. 'clients' => ['sort' => ['a' => 'desc']],
  236. ]);
  237. $expected['clients']['orders'] += ['fields' => ['a', 'b']];
  238. $expected['clients'] += ['sort' => ['a' => 'desc']];
  239. $this->assertEquals($expected, $loader->contain());
  240. }
  241. /**
  242. * Tests that it is possible to pass a function as the array value for contain
  243. *
  244. * @return void
  245. */
  246. public function testContainClosure() {
  247. $builder = function ($query) {
  248. };
  249. $loader = new EagerLoader;
  250. $loader->contain([
  251. 'clients.orders.stuff' => ['fields' => ['a']],
  252. 'clients' => $builder
  253. ]);
  254. $expected = [
  255. 'clients' => [
  256. 'orders' => [
  257. 'stuff' => ['fields' => ['a']]
  258. ],
  259. 'queryBuilder' => $builder
  260. ]
  261. ];
  262. $this->assertEquals($expected, $loader->contain());
  263. $loader = new EagerLoader;
  264. $loader->contain([
  265. 'clients.orders.stuff' => ['fields' => ['a']],
  266. 'clients' => ['queryBuilder' => $builder]
  267. ]);
  268. $this->assertEquals($expected, $loader->contain());
  269. }
  270. /**
  271. * Test that fields for contained models are aliased and added to the select clause
  272. *
  273. * @return void
  274. */
  275. public function testContainToFieldsPredefined() {
  276. $contains = [
  277. 'clients' => [
  278. 'fields' => ['name', 'company_id', 'clients.telephone'],
  279. 'orders' => [
  280. 'fields' => ['total', 'placed']
  281. ]
  282. ]
  283. ];
  284. $table = TableRegistry::get('foo');
  285. $query = new Query($this->connection, $table);
  286. $loader = new EagerLoader;
  287. $loader->contain($contains);
  288. $query->select('foo.id');
  289. $loader->attachAssociations($query, $table, true);
  290. $select = $query->clause('select');
  291. $expected = [
  292. 'foo.id', 'clients__name' => 'clients.name',
  293. 'clients__company_id' => 'clients.company_id',
  294. 'clients__telephone' => 'clients.telephone',
  295. 'orders__total' => 'orders.total', 'orders__placed' => 'orders.placed'
  296. ];
  297. $this->assertEquals($expected, $select);
  298. }
  299. /**
  300. * Tests that default fields for associations are added to the select clause when
  301. * none is specified
  302. *
  303. * @return void
  304. */
  305. public function testContainToFieldsDefault() {
  306. $contains = ['clients' => ['orders']];
  307. $query = new Query($this->connection, $this->table);
  308. $query->select()->contain($contains)->sql();
  309. $select = $query->clause('select');
  310. $expected = [
  311. 'foo__id' => 'foo.id', 'clients__name' => 'clients.name',
  312. 'clients__id' => 'clients.id', 'clients__phone' => 'clients.phone',
  313. 'orders__id' => 'orders.id', 'orders__total' => 'orders.total',
  314. 'orders__placed' => 'orders.placed'
  315. ];
  316. $expected = $this->_quoteArray($expected);
  317. $this->assertEquals($expected, $select);
  318. $contains['clients']['fields'] = ['name'];
  319. $query = new Query($this->connection, $this->table);
  320. $query->select('foo.id')->contain($contains)->sql();
  321. $select = $query->clause('select');
  322. $expected = ['foo__id' => 'foo.id', 'clients__name' => 'clients.name'];
  323. $expected = $this->_quoteArray($expected);
  324. $this->assertEquals($expected, $select);
  325. $contains['clients']['fields'] = [];
  326. $contains['clients']['orders']['fields'] = false;
  327. $query = new Query($this->connection, $this->table);
  328. $query->select()->contain($contains)->sql();
  329. $select = $query->clause('select');
  330. $expected = [
  331. 'foo__id' => 'foo.id',
  332. 'clients__id' => 'clients.id',
  333. 'clients__name' => 'clients.name',
  334. 'clients__phone' => 'clients.phone',
  335. ];
  336. $expected = $this->_quoteArray($expected);
  337. $this->assertEquals($expected, $select);
  338. }
  339. /**
  340. * Tests that the path for gettings to a deep assocition is materialized in an
  341. * array key
  342. *
  343. * @return void
  344. */
  345. public function testNormalizedPath() {
  346. $contains = [
  347. 'clients' => [
  348. 'orders' => [
  349. 'orderTypes',
  350. 'stuff' => ['stuffTypes']
  351. ],
  352. 'companies' => [
  353. 'categories'
  354. ]
  355. ]
  356. ];
  357. $query = $this->getMock(
  358. '\Cake\ORM\Query',
  359. ['join'],
  360. [$this->connection, $this->table]
  361. );
  362. $loader = new EagerLoader;
  363. $loader->contain($contains);
  364. $normalized = $loader->normalized($this->table);
  365. $this->assertEquals('clients', $normalized['clients']['aliasPath']);
  366. $this->assertEquals('client', $normalized['clients']['propertyPath']);
  367. $assocs = $normalized['clients']['associations'];
  368. $this->assertEquals('clients.orders', $assocs['orders']['aliasPath']);
  369. $this->assertEquals('client.order', $assocs['orders']['propertyPath']);
  370. $assocs = $assocs['orders']['associations'];
  371. $this->assertEquals('clients.orders.orderTypes', $assocs['orderTypes']['aliasPath']);
  372. $this->assertEquals('client.order.order_type', $assocs['orderTypes']['propertyPath']);
  373. $this->assertEquals('clients.orders.stuff', $assocs['stuff']['aliasPath']);
  374. $this->assertEquals('client.order.stuff', $assocs['stuff']['propertyPath']);
  375. $assocs = $assocs['stuff']['associations'];
  376. $this->assertEquals(
  377. 'clients.orders.stuff.stuffTypes',
  378. $assocs['stuffTypes']['aliasPath']
  379. );
  380. $this->assertEquals(
  381. 'client.order.stuff.stuff_type',
  382. $assocs['stuffTypes']['propertyPath']
  383. );
  384. }
  385. /**
  386. * Helper function sued to quoted both keys and values in an array in case
  387. * the test suite is running with auto quoting enabled
  388. *
  389. * @param array $elements
  390. * @return array
  391. */
  392. protected function _quoteArray($elements) {
  393. if ($this->connection->driver()->autoQuoting()) {
  394. $quoter = function ($e) {
  395. return $this->connection->driver()->quoteIdentifier($e);
  396. };
  397. return array_combine(
  398. array_map($quoter, array_keys($elements)),
  399. array_map($quoter, array_values($elements))
  400. );
  401. }
  402. return $elements;
  403. }
  404. }