BelongsToTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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\Association;
  16. use Cake\Database\Expression\IdentifierExpression;
  17. use Cake\Database\Expression\QueryExpression;
  18. use Cake\Database\TypeMap;
  19. use Cake\ORM\Association\BelongsTo;
  20. use Cake\ORM\Entity;
  21. use Cake\TestSuite\TestCase;
  22. /**
  23. * Tests BelongsTo class
  24. */
  25. class BelongsToTest extends TestCase
  26. {
  27. /**
  28. * Fixtures to use.
  29. *
  30. * @var array
  31. */
  32. public $fixtures = ['core.Articles', 'core.Authors', 'core.Comments'];
  33. /**
  34. * Set up
  35. *
  36. * @return void
  37. */
  38. public function setUp()
  39. {
  40. parent::setUp();
  41. $this->company = $this->getTableLocator()->get('Companies', [
  42. 'schema' => [
  43. 'id' => ['type' => 'integer'],
  44. 'company_name' => ['type' => 'string'],
  45. '_constraints' => [
  46. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  47. ],
  48. ],
  49. ]);
  50. $this->client = $this->getTableLocator()->get('Clients', [
  51. 'schema' => [
  52. 'id' => ['type' => 'integer'],
  53. 'client_name' => ['type' => 'string'],
  54. 'company_id' => ['type' => 'integer'],
  55. '_constraints' => [
  56. 'primary' => ['type' => 'primary', 'columns' => ['id']],
  57. ],
  58. ],
  59. ]);
  60. $this->companiesTypeMap = new TypeMap([
  61. 'Companies.id' => 'integer',
  62. 'id' => 'integer',
  63. 'Companies.company_name' => 'string',
  64. 'company_name' => 'string',
  65. 'Companies__id' => 'integer',
  66. 'Companies__company_name' => 'string',
  67. ]);
  68. }
  69. /**
  70. * Test that foreignKey generation
  71. *
  72. * @return void
  73. */
  74. public function testSetForeignKey()
  75. {
  76. $assoc = new BelongsTo('Companies', [
  77. 'sourceTable' => $this->client,
  78. 'targetTable' => $this->company,
  79. ]);
  80. $this->assertEquals('company_id', $assoc->getForeignKey());
  81. $this->assertSame($assoc, $assoc->setForeignKey('another_key'));
  82. $this->assertEquals('another_key', $assoc->getForeignKey());
  83. }
  84. /**
  85. * Test that foreignKey generation
  86. *
  87. * @group deprecated
  88. * @return void
  89. */
  90. public function testForeignKey()
  91. {
  92. $this->deprecated(function () {
  93. $assoc = new BelongsTo('Companies', [
  94. 'sourceTable' => $this->client,
  95. 'targetTable' => $this->company,
  96. ]);
  97. $this->assertEquals('company_id', $assoc->foreignKey());
  98. $this->assertEquals('another_key', $assoc->foreignKey('another_key'));
  99. $this->assertEquals('another_key', $assoc->foreignKey());
  100. });
  101. }
  102. /**
  103. * Test that foreignKey generation ignores database names in target table.
  104. *
  105. * @return void
  106. */
  107. public function testForeignKeyIgnoreDatabaseName()
  108. {
  109. $this->company->setTable('schema.companies');
  110. $this->client->setTable('schema.clients');
  111. $assoc = new BelongsTo('Companies', [
  112. 'sourceTable' => $this->client,
  113. 'targetTable' => $this->company,
  114. ]);
  115. $this->assertEquals('company_id', $assoc->getForeignKey());
  116. }
  117. /**
  118. * Tests that the association reports it can be joined
  119. *
  120. * @return void
  121. */
  122. public function testCanBeJoined()
  123. {
  124. $assoc = new BelongsTo('Test');
  125. $this->assertTrue($assoc->canBeJoined());
  126. }
  127. /**
  128. * Tests that the alias set on associations is actually on the Entity
  129. *
  130. * @return void
  131. */
  132. public function testCustomAlias()
  133. {
  134. $table = $this->getTableLocator()->get('Articles', [
  135. 'className' => 'TestPlugin.Articles',
  136. ]);
  137. $table->addAssociations([
  138. 'belongsTo' => [
  139. 'FooAuthors' => ['className' => 'TestPlugin.Authors', 'foreignKey' => 'author_id'],
  140. ],
  141. ]);
  142. $article = $table->find()->contain(['FooAuthors'])->first();
  143. $this->assertTrue(isset($article->foo_author));
  144. $this->assertEquals($article->foo_author->name, 'mariano');
  145. $this->assertNull($article->Authors);
  146. }
  147. /**
  148. * Tests that the correct join and fields are attached to a query depending on
  149. * the association config
  150. *
  151. * @return void
  152. */
  153. public function testAttachTo()
  154. {
  155. $config = [
  156. 'foreignKey' => 'company_id',
  157. 'sourceTable' => $this->client,
  158. 'targetTable' => $this->company,
  159. 'conditions' => ['Companies.is_active' => true],
  160. ];
  161. $association = new BelongsTo('Companies', $config);
  162. $query = $this->client->query();
  163. $association->attachTo($query);
  164. $expected = [
  165. 'Companies__id' => 'Companies.id',
  166. 'Companies__company_name' => 'Companies.company_name',
  167. ];
  168. $this->assertEquals($expected, $query->clause('select'));
  169. $expected = [
  170. 'Companies' => [
  171. 'alias' => 'Companies',
  172. 'table' => 'companies',
  173. 'type' => 'LEFT',
  174. 'conditions' => new QueryExpression([
  175. 'Companies.is_active' => true,
  176. ['Companies.id' => new IdentifierExpression('Clients.company_id')],
  177. ], $this->companiesTypeMap),
  178. ],
  179. ];
  180. $this->assertEquals($expected, $query->clause('join'));
  181. $this->assertEquals(
  182. 'integer',
  183. $query->getTypeMap()->type('Companies__id'),
  184. 'Associations should map types.'
  185. );
  186. }
  187. /**
  188. * Tests that it is possible to avoid fields inclusion for the associated table
  189. *
  190. * @return void
  191. */
  192. public function testAttachToNoFields()
  193. {
  194. $config = [
  195. 'sourceTable' => $this->client,
  196. 'targetTable' => $this->company,
  197. 'conditions' => ['Companies.is_active' => true],
  198. ];
  199. $query = $this->client->query();
  200. $association = new BelongsTo('Companies', $config);
  201. $association->attachTo($query, ['includeFields' => false]);
  202. $this->assertEmpty($query->clause('select'), 'no fields should be added.');
  203. }
  204. /**
  205. * Tests that using belongsto with a table having a multi column primary
  206. * key will work if the foreign key is passed
  207. *
  208. * @return void
  209. */
  210. public function testAttachToMultiPrimaryKey()
  211. {
  212. $this->company->setPrimaryKey(['id', 'tenant_id']);
  213. $config = [
  214. 'foreignKey' => ['company_id', 'company_tenant_id'],
  215. 'sourceTable' => $this->client,
  216. 'targetTable' => $this->company,
  217. 'conditions' => ['Companies.is_active' => true],
  218. ];
  219. $association = new BelongsTo('Companies', $config);
  220. $query = $this->client->query();
  221. $association->attachTo($query);
  222. $expected = [
  223. 'Companies__id' => 'Companies.id',
  224. 'Companies__company_name' => 'Companies.company_name',
  225. ];
  226. $this->assertEquals($expected, $query->clause('select'));
  227. $field1 = new IdentifierExpression('Clients.company_id');
  228. $field2 = new IdentifierExpression('Clients.company_tenant_id');
  229. $expected = [
  230. 'Companies' => [
  231. 'conditions' => new QueryExpression([
  232. 'Companies.is_active' => true,
  233. ['Companies.id' => $field1, 'Companies.tenant_id' => $field2],
  234. ], $this->companiesTypeMap),
  235. 'table' => 'companies',
  236. 'type' => 'LEFT',
  237. 'alias' => 'Companies',
  238. ],
  239. ];
  240. $this->assertEquals($expected, $query->clause('join'));
  241. }
  242. /**
  243. * Tests that using belongsto with a table having a multi column primary
  244. * key will work if the foreign key is passed
  245. *
  246. * @return void
  247. */
  248. public function testAttachToMultiPrimaryKeyMismatch()
  249. {
  250. $this->expectException(\RuntimeException::class);
  251. $this->expectExceptionMessage('Cannot match provided foreignKey for "Companies", got "(company_id)" but expected foreign key for "(id, tenant_id)"');
  252. $this->company->setPrimaryKey(['id', 'tenant_id']);
  253. $query = $this->client->query();
  254. $config = [
  255. 'foreignKey' => 'company_id',
  256. 'sourceTable' => $this->client,
  257. 'targetTable' => $this->company,
  258. 'conditions' => ['Companies.is_active' => true],
  259. ];
  260. $association = new BelongsTo('Companies', $config);
  261. $association->attachTo($query);
  262. }
  263. /**
  264. * Test the cascading delete of BelongsTo.
  265. *
  266. * @return void
  267. */
  268. public function testCascadeDelete()
  269. {
  270. $mock = $this->getMockBuilder('Cake\ORM\Table')
  271. ->disableOriginalConstructor()
  272. ->getMock();
  273. $config = [
  274. 'sourceTable' => $this->client,
  275. 'targetTable' => $mock,
  276. ];
  277. $mock->expects($this->never())
  278. ->method('find');
  279. $mock->expects($this->never())
  280. ->method('delete');
  281. $association = new BelongsTo('Companies', $config);
  282. $entity = new Entity(['company_name' => 'CakePHP', 'id' => 1]);
  283. $this->assertTrue($association->cascadeDelete($entity));
  284. }
  285. /**
  286. * Test that saveAssociated() ignores non entity values.
  287. *
  288. * @return void
  289. */
  290. public function testSaveAssociatedOnlyEntities()
  291. {
  292. $mock = $this->getMockBuilder('Cake\ORM\Table')
  293. ->setMethods(['saveAssociated'])
  294. ->disableOriginalConstructor()
  295. ->getMock();
  296. $config = [
  297. 'sourceTable' => $this->client,
  298. 'targetTable' => $mock,
  299. ];
  300. $mock->expects($this->never())
  301. ->method('saveAssociated');
  302. $entity = new Entity([
  303. 'title' => 'A Title',
  304. 'body' => 'A body',
  305. 'author' => ['name' => 'Jose'],
  306. ]);
  307. $association = new BelongsTo('Authors', $config);
  308. $result = $association->saveAssociated($entity);
  309. $this->assertSame($result, $entity);
  310. $this->assertNull($entity->author_id);
  311. }
  312. /**
  313. * Tests that property is being set using the constructor options.
  314. *
  315. * @return void
  316. */
  317. public function testPropertyOption()
  318. {
  319. $config = ['propertyName' => 'thing_placeholder'];
  320. $association = new BelongsTo('Thing', $config);
  321. $this->assertEquals('thing_placeholder', $association->getProperty());
  322. }
  323. /**
  324. * Test that plugin names are omitted from property()
  325. *
  326. * @return void
  327. */
  328. public function testPropertyNoPlugin()
  329. {
  330. $mock = $this->getMockBuilder('Cake\ORM\Table')
  331. ->disableOriginalConstructor()
  332. ->getMock();
  333. $config = [
  334. 'sourceTable' => $this->client,
  335. 'targetTable' => $mock,
  336. ];
  337. $association = new BelongsTo('Contacts.Companies', $config);
  338. $this->assertEquals('company', $association->getProperty());
  339. }
  340. /**
  341. * Tests that attaching an association to a query will trigger beforeFind
  342. * for the target table
  343. *
  344. * @return void
  345. */
  346. public function testAttachToBeforeFind()
  347. {
  348. $config = [
  349. 'foreignKey' => 'company_id',
  350. 'sourceTable' => $this->client,
  351. 'targetTable' => $this->company,
  352. ];
  353. $listener = $this->getMockBuilder('stdClass')
  354. ->setMethods(['__invoke'])
  355. ->getMock();
  356. $this->company->getEventManager()->on('Model.beforeFind', $listener);
  357. $association = new BelongsTo('Companies', $config);
  358. $listener->expects($this->once())->method('__invoke')
  359. ->with(
  360. $this->isInstanceOf('\Cake\Event\Event'),
  361. $this->isInstanceOf('\Cake\ORM\Query'),
  362. $this->isInstanceOf('\ArrayObject'),
  363. false
  364. );
  365. $association->attachTo($this->client->query());
  366. }
  367. /**
  368. * Tests that attaching an association to a query will trigger beforeFind
  369. * for the target table
  370. *
  371. * @return void
  372. */
  373. public function testAttachToBeforeFindExtraOptions()
  374. {
  375. $config = [
  376. 'foreignKey' => 'company_id',
  377. 'sourceTable' => $this->client,
  378. 'targetTable' => $this->company,
  379. ];
  380. $listener = $this->getMockBuilder('stdClass')
  381. ->setMethods(['__invoke'])
  382. ->getMock();
  383. $this->company->getEventManager()->on('Model.beforeFind', $listener);
  384. $association = new BelongsTo('Companies', $config);
  385. $options = new \ArrayObject(['something' => 'more']);
  386. $listener->expects($this->once())->method('__invoke')
  387. ->with(
  388. $this->isInstanceOf('\Cake\Event\Event'),
  389. $this->isInstanceOf('\Cake\ORM\Query'),
  390. $options,
  391. false
  392. );
  393. $query = $this->client->query();
  394. $association->attachTo($query, ['queryBuilder' => function ($q) {
  395. return $q->applyOptions(['something' => 'more']);
  396. }]);
  397. }
  398. /**
  399. * Test that failing to add the foreignKey to the list of fields will throw an
  400. * exception
  401. *
  402. * @return void
  403. */
  404. public function testAttachToNoFieldsSelected()
  405. {
  406. $articles = $this->getTableLocator()->get('Articles');
  407. $association = $articles->belongsTo('Authors');
  408. $query = $articles->find()
  409. ->select(['Authors.name'])
  410. ->where(['Articles.id' => 1])
  411. ->contain('Authors');
  412. $result = $query->firstOrFail();
  413. $this->assertNotEmpty($result->author);
  414. $this->assertSame('mariano', $result->author->name);
  415. $this->assertSame(['author'], array_keys($result->toArray()), 'No other properties included.');
  416. }
  417. /**
  418. * Test that formatResults in a joined association finder doesn't dirty
  419. * the root entity.
  420. *
  421. * @return void
  422. */
  423. public function testAttachToFormatResultsNoDirtyResults()
  424. {
  425. $this->setAppNamespace('TestApp');
  426. $articles = $this->getTableLocator()->get('Articles');
  427. $articles->belongsTo('Authors')
  428. ->setFinder('formatted');
  429. $query = $articles->find()
  430. ->where(['Articles.id' => 1])
  431. ->contain('Authors');
  432. $result = $query->firstOrFail();
  433. $this->assertNotEmpty($result->author);
  434. $this->assertNotEmpty($result->author->formatted);
  435. $this->assertFalse($result->isDirty(), 'Record should be clean as it was pulled from the db.');
  436. }
  437. }