HasOneTest.php 11 KB

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