HasOneTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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\Association;
  16. use Cake\Database\Expression\IdentifierExpression;
  17. use Cake\Database\Expression\QueryExpression;
  18. use Cake\Database\TypeMap;
  19. use Cake\ORM\Association\HasOne;
  20. use Cake\ORM\Entity;
  21. use Cake\ORM\TableRegistry;
  22. use Cake\TestSuite\TestCase;
  23. /**
  24. * Tests HasOne class
  25. */
  26. class HasOneTest extends TestCase
  27. {
  28. /**
  29. * Set up
  30. *
  31. * @return void
  32. */
  33. public function setUp()
  34. {
  35. parent::setUp();
  36. $this->user = TableRegistry::get('Users', [
  37. 'schema' => [
  38. 'id' => ['type' => 'integer'],
  39. 'username' => ['type' => 'string'],
  40. '_constraints' => [
  41. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  42. ]
  43. ]
  44. ]);
  45. $this->profile = TableRegistry::get('Profiles', [
  46. 'schema' => [
  47. 'id' => ['type' => 'integer'],
  48. 'first_name' => ['type' => 'string'],
  49. 'user_id' => ['type' => 'integer'],
  50. '_constraints' => [
  51. 'primary' => ['type' => 'primary', 'columns' => ['id']]
  52. ]
  53. ]
  54. ]);
  55. $this->profilesTypeMap = new TypeMap([
  56. 'Profiles.id' => 'integer',
  57. 'id' => 'integer',
  58. 'Profiles.first_name' => 'string',
  59. 'first_name' => 'string',
  60. 'Profiles.user_id' => 'integer',
  61. 'user_id' => 'integer',
  62. 'Profiles__first_name' => 'string',
  63. 'Profiles__user_id' => 'integer',
  64. 'Profiles__id' => 'integer',
  65. ]);
  66. }
  67. /**
  68. * Tear down
  69. *
  70. * @return void
  71. */
  72. public function tearDown()
  73. {
  74. parent::tearDown();
  75. TableRegistry::clear();
  76. }
  77. /**
  78. * Tests that foreignKey() returns the correct configured value
  79. *
  80. * @return void
  81. */
  82. public function testForeignKey()
  83. {
  84. $assoc = new HasOne('Profiles', [
  85. 'sourceTable' => $this->user
  86. ]);
  87. $this->assertEquals('user_id', $assoc->foreignKey());
  88. $this->assertEquals('another_key', $assoc->foreignKey('another_key'));
  89. $this->assertEquals('another_key', $assoc->foreignKey());
  90. }
  91. /**
  92. * Tests that the association reports it can be joined
  93. *
  94. * @return void
  95. */
  96. public function testCanBeJoined()
  97. {
  98. $assoc = new HasOne('Test');
  99. $this->assertTrue($assoc->canBeJoined());
  100. }
  101. /**
  102. * Tests that the correct join and fields are attached to a query depending on
  103. * the association config
  104. *
  105. * @return void
  106. */
  107. public function testAttachTo()
  108. {
  109. $query = $this->getMockBuilder('\Cake\ORM\Query')
  110. ->setMethods(['join', 'select'])
  111. ->setConstructorArgs([null, null])
  112. ->getMock();
  113. $config = [
  114. 'foreignKey' => 'user_id',
  115. 'sourceTable' => $this->user,
  116. 'targetTable' => $this->profile,
  117. 'conditions' => ['Profiles.is_active' => true]
  118. ];
  119. $association = new HasOne('Profiles', $config);
  120. $field = new IdentifierExpression('Profiles.user_id');
  121. $query->expects($this->once())->method('join')->with([
  122. 'Profiles' => [
  123. 'conditions' => new QueryExpression([
  124. 'Profiles.is_active' => true,
  125. ['Users.id' => $field],
  126. ], $this->profilesTypeMap),
  127. 'type' => 'LEFT',
  128. 'table' => 'profiles'
  129. ]
  130. ]);
  131. $query->expects($this->once())->method('select')->with([
  132. 'Profiles__id' => 'Profiles.id',
  133. 'Profiles__first_name' => 'Profiles.first_name',
  134. 'Profiles__user_id' => 'Profiles.user_id'
  135. ]);
  136. $association->attachTo($query);
  137. $this->assertEquals(
  138. 'string',
  139. $query->typeMap()->type('Profiles__first_name'),
  140. 'Associations should map types.'
  141. );
  142. }
  143. /**
  144. * Tests that it is possible to avoid fields inclusion for the associated table
  145. *
  146. * @return void
  147. */
  148. public function testAttachToNoFields()
  149. {
  150. $query = $this->getMockBuilder('\Cake\ORM\Query')
  151. ->setMethods(['join', 'select'])
  152. ->setConstructorArgs([null, null])
  153. ->getMock();
  154. $config = [
  155. 'sourceTable' => $this->user,
  156. 'targetTable' => $this->profile,
  157. 'conditions' => ['Profiles.is_active' => true]
  158. ];
  159. $association = new HasOne('Profiles', $config);
  160. $field = new IdentifierExpression('Profiles.user_id');
  161. $query->expects($this->once())->method('join')->with([
  162. 'Profiles' => [
  163. 'conditions' => new QueryExpression([
  164. 'Profiles.is_active' => true,
  165. ['Users.id' => $field],
  166. ], $this->profilesTypeMap),
  167. 'type' => 'LEFT',
  168. 'table' => 'profiles'
  169. ]
  170. ]);
  171. $query->expects($this->never())->method('select');
  172. $association->attachTo($query, ['includeFields' => false]);
  173. }
  174. /**
  175. * Tests that using hasOne with a table having a multi column primary
  176. * key will work if the foreign key is passed
  177. *
  178. * @return void
  179. */
  180. public function testAttachToMultiPrimaryKey()
  181. {
  182. $query = $this->getMockBuilder('\Cake\ORM\Query')
  183. ->setMethods(['join', 'select'])
  184. ->setConstructorArgs([null, null])
  185. ->getMock();
  186. $config = [
  187. 'sourceTable' => $this->user,
  188. 'targetTable' => $this->profile,
  189. 'conditions' => ['Profiles.is_active' => true],
  190. 'foreignKey' => ['user_id', 'user_site_id']
  191. ];
  192. $this->user->primaryKey(['id', 'site_id']);
  193. $association = new HasOne('Profiles', $config);
  194. $field1 = new IdentifierExpression('Profiles.user_id');
  195. $field2 = new IdentifierExpression('Profiles.user_site_id');
  196. $query->expects($this->once())->method('join')->with([
  197. 'Profiles' => [
  198. 'conditions' => new QueryExpression([
  199. 'Profiles.is_active' => true,
  200. ['Users.id' => $field1, 'Users.site_id' => $field2],
  201. ], $this->profilesTypeMap),
  202. 'type' => 'LEFT',
  203. 'table' => 'profiles'
  204. ]
  205. ]);
  206. $query->expects($this->never())->method('select');
  207. $association->attachTo($query, ['includeFields' => false]);
  208. }
  209. /**
  210. * Tests that using hasOne with a table having a multi column primary
  211. * key will work if the foreign key is passed
  212. *
  213. * @expectedException \RuntimeException
  214. * @expectedExceptionMessage Cannot match provided foreignKey for "Profiles", got "(user_id)" but expected foreign key for "(id, site_id)"
  215. * @return void
  216. */
  217. public function testAttachToMultiPrimaryKeyMistmatch()
  218. {
  219. $query = $this->getMockBuilder('\Cake\ORM\Query')
  220. ->setMethods(['join', 'select'])
  221. ->setConstructorArgs([null, null])
  222. ->getMock();
  223. $config = [
  224. 'sourceTable' => $this->user,
  225. 'targetTable' => $this->profile,
  226. 'conditions' => ['Profiles.is_active' => true],
  227. ];
  228. $this->user->primaryKey(['id', 'site_id']);
  229. $association = new HasOne('Profiles', $config);
  230. $association->attachTo($query, ['includeFields' => false]);
  231. }
  232. /**
  233. * Test that saveAssociated() ignores non entity values.
  234. *
  235. * @return void
  236. */
  237. public function testSaveAssociatedOnlyEntities()
  238. {
  239. $mock = $this->getMockBuilder('Cake\ORM\Table')
  240. ->setMethods(['saveAssociated'])
  241. ->disableOriginalConstructor()
  242. ->getMock();
  243. $config = [
  244. 'sourceTable' => $this->user,
  245. 'targetTable' => $mock,
  246. ];
  247. $mock->expects($this->never())
  248. ->method('saveAssociated');
  249. $entity = new Entity([
  250. 'username' => 'Mark',
  251. 'email' => 'mark@example.com',
  252. 'profile' => ['twitter' => '@cakephp']
  253. ]);
  254. $association = new HasOne('Profiles', $config);
  255. $result = $association->saveAssociated($entity);
  256. $this->assertSame($result, $entity);
  257. }
  258. /**
  259. * Tests that property is being set using the constructor options.
  260. *
  261. * @return void
  262. */
  263. public function testPropertyOption()
  264. {
  265. $config = ['propertyName' => 'thing_placeholder'];
  266. $association = new hasOne('Thing', $config);
  267. $this->assertEquals('thing_placeholder', $association->property());
  268. }
  269. /**
  270. * Test that plugin names are omitted from property()
  271. *
  272. * @return void
  273. */
  274. public function testPropertyNoPlugin()
  275. {
  276. $mock = $this->getMockBuilder('Cake\ORM\Table')
  277. ->disableOriginalConstructor()
  278. ->getMock();
  279. $config = [
  280. 'sourceTable' => $this->user,
  281. 'targetTable' => $mock,
  282. ];
  283. $association = new HasOne('Contacts.Profiles', $config);
  284. $this->assertEquals('profile', $association->property());
  285. }
  286. /**
  287. * Tests that attaching an association to a query will trigger beforeFind
  288. * for the target table
  289. *
  290. * @return void
  291. */
  292. public function testAttachToBeforeFind()
  293. {
  294. $query = $this->getMockBuilder('\Cake\ORM\Query')
  295. ->setMethods(['join', 'select'])
  296. ->setConstructorArgs([null, null])
  297. ->getMock();
  298. $config = [
  299. 'foreignKey' => 'user_id',
  300. 'sourceTable' => $this->user,
  301. 'targetTable' => $this->profile,
  302. ];
  303. $listener = $this->getMockBuilder('stdClass')
  304. ->setMethods(['__invoke'])
  305. ->getMock();
  306. $this->profile->eventManager()->attach($listener, 'Model.beforeFind');
  307. $association = new HasOne('Profiles', $config);
  308. $listener->expects($this->once())->method('__invoke')
  309. ->with(
  310. $this->isInstanceOf('\Cake\Event\Event'),
  311. $this->isInstanceOf('\Cake\ORM\Query'),
  312. $this->isInstanceOf('\ArrayObject'),
  313. false
  314. );
  315. $association->attachTo($query);
  316. }
  317. /**
  318. * Tests that attaching an association to a query will trigger beforeFind
  319. * for the target table
  320. *
  321. * @return void
  322. */
  323. public function testAttachToBeforeFindExtraOptions()
  324. {
  325. $query = $this->getMockBuilder('\Cake\ORM\Query')
  326. ->setMethods(['join', 'select'])
  327. ->setConstructorArgs([null, null])
  328. ->getMock();
  329. $config = [
  330. 'foreignKey' => 'user_id',
  331. 'sourceTable' => $this->user,
  332. 'targetTable' => $this->profile,
  333. ];
  334. $listener = $this->getMockBuilder('stdClass')
  335. ->setMethods(['__invoke'])
  336. ->getMock();
  337. $this->profile->eventManager()->attach($listener, 'Model.beforeFind');
  338. $association = new HasOne('Profiles', $config);
  339. $opts = new \ArrayObject(['something' => 'more']);
  340. $listener->expects($this->once())->method('__invoke')
  341. ->with(
  342. $this->isInstanceOf('\Cake\Event\Event'),
  343. $this->isInstanceOf('\Cake\ORM\Query'),
  344. $opts,
  345. false
  346. );
  347. $association->attachTo($query, ['queryBuilder' => function ($q) {
  348. return $q->applyOptions(['something' => 'more']);
  349. }]);
  350. }
  351. }