DatabaseEloquentRelationTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. namespace Illuminate\Tests\Database;
  3. use Exception;
  4. use Illuminate\Database\Eloquent\Builder;
  5. use Illuminate\Database\Eloquent\Casts\Attribute;
  6. use Illuminate\Database\Eloquent\Collection;
  7. use Illuminate\Database\Eloquent\Model;
  8. use Illuminate\Database\Eloquent\Relations\HasOne;
  9. use Illuminate\Database\Eloquent\Relations\Relation;
  10. use Illuminate\Support\Carbon;
  11. use Mockery as m;
  12. use PHPUnit\Framework\TestCase;
  13. class DatabaseEloquentRelationTest extends TestCase
  14. {
  15. protected function tearDown(): void
  16. {
  17. m::close();
  18. }
  19. public function testSetRelationFail()
  20. {
  21. $parent = new EloquentRelationResetModelStub;
  22. $relation = new EloquentRelationResetModelStub;
  23. $parent->setRelation('test', $relation);
  24. $parent->setRelation('foo', 'bar');
  25. $this->assertArrayNotHasKey('foo', $parent->toArray());
  26. }
  27. public function testUnsetExistingRelation()
  28. {
  29. $parent = new EloquentRelationResetModelStub;
  30. $relation = new EloquentRelationResetModelStub;
  31. $parent->setRelation('foo', $relation);
  32. $parent->unsetRelation('foo');
  33. $this->assertFalse($parent->relationLoaded('foo'));
  34. }
  35. public function testTouchMethodUpdatesRelatedTimestamps()
  36. {
  37. $builder = m::mock(Builder::class);
  38. $parent = m::mock(Model::class);
  39. $parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
  40. $related = m::mock(EloquentNoTouchingModelStub::class)->makePartial();
  41. $builder->shouldReceive('getModel')->andReturn($related);
  42. $builder->shouldReceive('whereNotNull');
  43. $builder->shouldReceive('where');
  44. $builder->shouldReceive('withoutGlobalScopes')->andReturn($builder);
  45. $relation = new HasOne($builder, $parent, 'foreign_key', 'id');
  46. $related->shouldReceive('getTable')->andReturn('table');
  47. $related->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at');
  48. $now = Carbon::now();
  49. $related->shouldReceive('freshTimestampString')->andReturn($now);
  50. $builder->shouldReceive('update')->once()->with(['updated_at' => $now]);
  51. $relation->touch();
  52. }
  53. public function testCanDisableParentTouchingForAllModels()
  54. {
  55. /** @var \Illuminate\Tests\Database\EloquentNoTouchingModelStub $related */
  56. $related = m::mock(EloquentNoTouchingModelStub::class)->makePartial();
  57. $related->shouldReceive('getUpdatedAtColumn')->never();
  58. $related->shouldReceive('freshTimestampString')->never();
  59. $this->assertFalse($related::isIgnoringTouch());
  60. Model::withoutTouching(function () use ($related) {
  61. $this->assertTrue($related::isIgnoringTouch());
  62. $builder = m::mock(Builder::class);
  63. $parent = m::mock(Model::class);
  64. $parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
  65. $builder->shouldReceive('getModel')->andReturn($related);
  66. $builder->shouldReceive('whereNotNull');
  67. $builder->shouldReceive('where');
  68. $builder->shouldReceive('withoutGlobalScopes')->andReturn($builder);
  69. $relation = new HasOne($builder, $parent, 'foreign_key', 'id');
  70. $builder->shouldReceive('update')->never();
  71. $relation->touch();
  72. });
  73. $this->assertFalse($related::isIgnoringTouch());
  74. }
  75. public function testCanDisableTouchingForSpecificModel()
  76. {
  77. $related = m::mock(EloquentNoTouchingModelStub::class)->makePartial();
  78. $related->shouldReceive('getUpdatedAtColumn')->never();
  79. $related->shouldReceive('freshTimestampString')->never();
  80. $anotherRelated = m::mock(EloquentNoTouchingAnotherModelStub::class)->makePartial();
  81. $this->assertFalse($related::isIgnoringTouch());
  82. $this->assertFalse($anotherRelated::isIgnoringTouch());
  83. EloquentNoTouchingModelStub::withoutTouching(function () use ($related, $anotherRelated) {
  84. $this->assertTrue($related::isIgnoringTouch());
  85. $this->assertFalse($anotherRelated::isIgnoringTouch());
  86. $builder = m::mock(Builder::class);
  87. $parent = m::mock(Model::class);
  88. $parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
  89. $builder->shouldReceive('getModel')->andReturn($related);
  90. $builder->shouldReceive('whereNotNull');
  91. $builder->shouldReceive('where');
  92. $builder->shouldReceive('withoutGlobalScopes')->andReturnSelf();
  93. $relation = new HasOne($builder, $parent, 'foreign_key', 'id');
  94. $builder->shouldReceive('update')->never();
  95. $relation->touch();
  96. $anotherBuilder = m::mock(Builder::class);
  97. $anotherParent = m::mock(Model::class);
  98. $anotherParent->shouldReceive('getAttribute')->with('id')->andReturn(2);
  99. $anotherBuilder->shouldReceive('getModel')->andReturn($anotherRelated);
  100. $anotherBuilder->shouldReceive('whereNotNull');
  101. $anotherBuilder->shouldReceive('where');
  102. $anotherBuilder->shouldReceive('withoutGlobalScopes')->andReturnSelf();
  103. $anotherRelation = new HasOne($anotherBuilder, $anotherParent, 'foreign_key', 'id');
  104. $now = Carbon::now();
  105. $anotherRelated->shouldReceive('freshTimestampString')->andReturn($now);
  106. $anotherBuilder->shouldReceive('update')->once()->with(['updated_at' => $now]);
  107. $anotherRelation->touch();
  108. });
  109. $this->assertFalse($related::isIgnoringTouch());
  110. $this->assertFalse($anotherRelated::isIgnoringTouch());
  111. }
  112. public function testParentModelIsNotTouchedWhenChildModelIsIgnored()
  113. {
  114. $related = m::mock(EloquentNoTouchingModelStub::class)->makePartial();
  115. $related->shouldReceive('getUpdatedAtColumn')->never();
  116. $related->shouldReceive('freshTimestampString')->never();
  117. $relatedChild = m::mock(EloquentNoTouchingChildModelStub::class)->makePartial();
  118. $relatedChild->shouldReceive('getUpdatedAtColumn')->never();
  119. $relatedChild->shouldReceive('freshTimestampString')->never();
  120. $this->assertFalse($related::isIgnoringTouch());
  121. $this->assertFalse($relatedChild::isIgnoringTouch());
  122. EloquentNoTouchingModelStub::withoutTouching(function () use ($related, $relatedChild) {
  123. $this->assertTrue($related::isIgnoringTouch());
  124. $this->assertTrue($relatedChild::isIgnoringTouch());
  125. $builder = m::mock(Builder::class);
  126. $parent = m::mock(Model::class);
  127. $parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
  128. $builder->shouldReceive('getModel')->andReturn($related);
  129. $builder->shouldReceive('whereNotNull');
  130. $builder->shouldReceive('where');
  131. $builder->shouldReceive('withoutGlobalScopes')->andReturnSelf();
  132. $relation = new HasOne($builder, $parent, 'foreign_key', 'id');
  133. $builder->shouldReceive('update')->never();
  134. $relation->touch();
  135. $anotherBuilder = m::mock(Builder::class);
  136. $anotherParent = m::mock(Model::class);
  137. $anotherParent->shouldReceive('getAttribute')->with('id')->andReturn(2);
  138. $anotherBuilder->shouldReceive('getModel')->andReturn($relatedChild);
  139. $anotherBuilder->shouldReceive('whereNotNull');
  140. $anotherBuilder->shouldReceive('where');
  141. $anotherBuilder->shouldReceive('withoutGlobalScopes')->andReturnSelf();
  142. $anotherRelation = new HasOne($anotherBuilder, $anotherParent, 'foreign_key', 'id');
  143. $anotherBuilder->shouldReceive('update')->never();
  144. $anotherRelation->touch();
  145. });
  146. $this->assertFalse($related::isIgnoringTouch());
  147. $this->assertFalse($relatedChild::isIgnoringTouch());
  148. }
  149. public function testIgnoredModelsStateIsResetWhenThereAreExceptions()
  150. {
  151. $related = m::mock(EloquentNoTouchingModelStub::class)->makePartial();
  152. $related->shouldReceive('getUpdatedAtColumn')->never();
  153. $related->shouldReceive('freshTimestampString')->never();
  154. $relatedChild = m::mock(EloquentNoTouchingChildModelStub::class)->makePartial();
  155. $relatedChild->shouldReceive('getUpdatedAtColumn')->never();
  156. $relatedChild->shouldReceive('freshTimestampString')->never();
  157. $this->assertFalse($related::isIgnoringTouch());
  158. $this->assertFalse($relatedChild::isIgnoringTouch());
  159. try {
  160. EloquentNoTouchingModelStub::withoutTouching(function () use ($related, $relatedChild) {
  161. $this->assertTrue($related::isIgnoringTouch());
  162. $this->assertTrue($relatedChild::isIgnoringTouch());
  163. throw new Exception;
  164. });
  165. $this->fail('Exception was not thrown');
  166. } catch (Exception $exception) {
  167. // Does nothing.
  168. }
  169. $this->assertFalse($related::isIgnoringTouch());
  170. $this->assertFalse($relatedChild::isIgnoringTouch());
  171. }
  172. public function testSettingMorphMapWithNumericArrayUsesTheTableNames()
  173. {
  174. Relation::morphMap([EloquentRelationResetModelStub::class]);
  175. $this->assertEquals([
  176. 'reset' => EloquentRelationResetModelStub::class,
  177. ], Relation::morphMap());
  178. Relation::morphMap([], false);
  179. }
  180. public function testSettingMorphMapWithNumericKeys()
  181. {
  182. Relation::morphMap([1 => 'App\User']);
  183. $this->assertEquals([
  184. 1 => 'App\User',
  185. ], Relation::morphMap());
  186. Relation::morphMap([], false);
  187. }
  188. public function testWithoutRelations()
  189. {
  190. $original = new EloquentNoTouchingModelStub;
  191. $original->setRelation('foo', 'baz');
  192. $this->assertSame('baz', $original->getRelation('foo'));
  193. $model = $original->withoutRelations();
  194. $this->assertInstanceOf(EloquentNoTouchingModelStub::class, $model);
  195. $this->assertTrue($original->relationLoaded('foo'));
  196. $this->assertFalse($model->relationLoaded('foo'));
  197. $model = $original->unsetRelations();
  198. $this->assertInstanceOf(EloquentNoTouchingModelStub::class, $model);
  199. $this->assertFalse($original->relationLoaded('foo'));
  200. $this->assertFalse($model->relationLoaded('foo'));
  201. }
  202. public function testMacroable()
  203. {
  204. Relation::macro('foo', function () {
  205. return 'foo';
  206. });
  207. $model = new EloquentRelationResetModelStub;
  208. $relation = new EloquentRelationStub($model->newQuery(), $model);
  209. $result = $relation->foo();
  210. $this->assertSame('foo', $result);
  211. }
  212. public function testRelationResolvers()
  213. {
  214. $model = new EloquentRelationResetModelStub;
  215. $builder = m::mock(Builder::class);
  216. $builder->shouldReceive('getModel')->andReturn($model);
  217. EloquentRelationResetModelStub::resolveRelationUsing('customer', function ($model) use ($builder) {
  218. return new EloquentResolverRelationStub($builder, $model);
  219. });
  220. $this->assertInstanceOf(EloquentResolverRelationStub::class, $model->customer());
  221. $this->assertSame(['key' => 'value'], $model->customer);
  222. }
  223. public function testIsRelationIgnoresAttribute()
  224. {
  225. $model = new EloquentRelationAndAtrributeModelStub;
  226. $this->assertTrue($model->isRelation('parent'));
  227. $this->assertFalse($model->isRelation('field'));
  228. }
  229. }
  230. class EloquentRelationResetModelStub extends Model
  231. {
  232. protected $table = 'reset';
  233. // Override method call which would normally go through __call()
  234. public function getQuery()
  235. {
  236. return $this->newQuery()->getQuery();
  237. }
  238. }
  239. class EloquentRelationStub extends Relation
  240. {
  241. public function addConstraints()
  242. {
  243. //
  244. }
  245. public function addEagerConstraints(array $models)
  246. {
  247. //
  248. }
  249. public function initRelation(array $models, $relation)
  250. {
  251. //
  252. }
  253. public function match(array $models, Collection $results, $relation)
  254. {
  255. //
  256. }
  257. public function getResults()
  258. {
  259. //
  260. }
  261. }
  262. class EloquentNoTouchingModelStub extends Model
  263. {
  264. protected $table = 'table';
  265. protected $attributes = [
  266. 'id' => 1,
  267. ];
  268. }
  269. class EloquentNoTouchingChildModelStub extends EloquentNoTouchingModelStub
  270. {
  271. //
  272. }
  273. class EloquentNoTouchingAnotherModelStub extends Model
  274. {
  275. protected $table = 'another_table';
  276. protected $attributes = [
  277. 'id' => 2,
  278. ];
  279. }
  280. class EloquentResolverRelationStub extends EloquentRelationStub
  281. {
  282. public function getResults()
  283. {
  284. return ['key' => 'value'];
  285. }
  286. }
  287. class EloquentRelationAndAtrributeModelStub extends Model
  288. {
  289. protected $table = 'one_more_table';
  290. public function field(): Attribute
  291. {
  292. return new Attribute(
  293. function ($value) {
  294. return $value;
  295. },
  296. function ($value) {
  297. return $value;
  298. },
  299. );
  300. }
  301. public function parent()
  302. {
  303. return $this->belongsTo(self::class);
  304. }
  305. }