EloquentBelongsToManyTest.php 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <?php
  2. namespace Illuminate\Tests\Integration\Database\EloquentBelongsToManyTest;
  3. use Illuminate\Database\Eloquent\Collection;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Database\Eloquent\ModelNotFoundException;
  6. use Illuminate\Database\Eloquent\Relations\Pivot;
  7. use Illuminate\Database\Schema\Blueprint;
  8. use Illuminate\Support\Carbon;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Schema;
  11. use Illuminate\Support\Str;
  12. use Illuminate\Tests\Integration\Database\DatabaseTestCase;
  13. class EloquentBelongsToManyTest extends DatabaseTestCase
  14. {
  15. protected function defineDatabaseMigrationsAfterDatabaseRefreshed()
  16. {
  17. Schema::create('users', function (Blueprint $table) {
  18. $table->increments('id');
  19. $table->string('uuid');
  20. $table->string('name');
  21. $table->timestamps();
  22. });
  23. Schema::create('posts', function (Blueprint $table) {
  24. $table->increments('id');
  25. $table->string('uuid');
  26. $table->string('title');
  27. $table->timestamps();
  28. });
  29. Schema::create('tags', function (Blueprint $table) {
  30. $table->increments('id');
  31. $table->string('name');
  32. $table->timestamps();
  33. });
  34. Schema::create('users_posts', function (Blueprint $table) {
  35. $table->increments('id');
  36. $table->string('user_uuid');
  37. $table->string('post_uuid');
  38. $table->tinyInteger('is_draft')->default(1);
  39. $table->timestamps();
  40. });
  41. Schema::create('posts_tags', function (Blueprint $table) {
  42. $table->integer('post_id');
  43. $table->integer('tag_id')->default(0);
  44. $table->string('tag_name')->default('')->nullable();
  45. $table->string('flag')->default('')->nullable();
  46. $table->timestamps();
  47. });
  48. }
  49. public function testBasicCreateAndRetrieve()
  50. {
  51. Carbon::setTestNow('2017-10-10 10:10:10');
  52. $post = Post::create(['title' => Str::random()]);
  53. $tag = Tag::create(['name' => Str::random()]);
  54. $tag2 = Tag::create(['name' => Str::random()]);
  55. $tag3 = Tag::create(['name' => Str::random()]);
  56. $post->tags()->sync([
  57. $tag->id => ['flag' => 'taylor'],
  58. $tag2->id => ['flag' => ''],
  59. $tag3->id => ['flag' => 'exclude'],
  60. ]);
  61. // Tags with flag = exclude should be excluded
  62. $this->assertCount(2, $post->tags);
  63. $this->assertInstanceOf(Collection::class, $post->tags);
  64. $this->assertEquals($tag->name, $post->tags[0]->name);
  65. $this->assertEquals($tag2->name, $post->tags[1]->name);
  66. // Testing on the pivot model
  67. $this->assertInstanceOf(Pivot::class, $post->tags[0]->pivot);
  68. $this->assertEquals($post->id, $post->tags[0]->pivot->post_id);
  69. $this->assertSame('post_id', $post->tags[0]->pivot->getForeignKey());
  70. $this->assertSame('tag_id', $post->tags[0]->pivot->getOtherKey());
  71. $this->assertSame('posts_tags', $post->tags[0]->pivot->getTable());
  72. $this->assertEquals(
  73. [
  74. 'post_id' => '1', 'tag_id' => '1', 'flag' => 'taylor',
  75. 'created_at' => '2017-10-10T10:10:10.000000Z', 'updated_at' => '2017-10-10T10:10:10.000000Z',
  76. ],
  77. $post->tags[0]->pivot->toArray()
  78. );
  79. }
  80. public function testRefreshOnOtherModelWorks()
  81. {
  82. $post = Post::create(['title' => Str::random()]);
  83. $tag = Tag::create(['name' => $tagName = Str::random()]);
  84. $post->tags()->sync([
  85. $tag->id,
  86. ]);
  87. $post->load('tags');
  88. $loadedTag = $post->tags()->first();
  89. $tag->update(['name' => 'newName']);
  90. $this->assertEquals($tagName, $loadedTag->name);
  91. $this->assertEquals($tagName, $post->tags[0]->name);
  92. $loadedTag->refresh();
  93. $this->assertSame('newName', $loadedTag->name);
  94. $post->refresh();
  95. $this->assertSame('newName', $post->tags[0]->name);
  96. }
  97. public function testCustomPivotClass()
  98. {
  99. Carbon::setTestNow('2017-10-10 10:10:10');
  100. $post = Post::create(['title' => Str::random()]);
  101. $tag = TagWithCustomPivot::create(['name' => Str::random()]);
  102. $post->tagsWithCustomPivot()->attach($tag->id);
  103. $this->assertInstanceOf(PostTagPivot::class, $post->tagsWithCustomPivot[0]->pivot);
  104. $this->assertEquals('1507630210', $post->tagsWithCustomPivot[0]->pivot->created_at);
  105. $this->assertInstanceOf(PostTagPivot::class, $post->tagsWithCustomPivotClass[0]->pivot);
  106. $this->assertSame('posts_tags', $post->tagsWithCustomPivotClass()->getTable());
  107. $this->assertEquals([
  108. 'post_id' => '1',
  109. 'tag_id' => '1',
  110. ], $post->tagsWithCustomAccessor[0]->tag->toArray());
  111. $pivot = $post->tagsWithCustomPivot[0]->pivot;
  112. $pivot->tag_id = 2;
  113. $pivot->save();
  114. $this->assertEquals(1, PostTagPivot::count());
  115. $this->assertEquals(1, PostTagPivot::first()->post_id);
  116. $this->assertEquals(2, PostTagPivot::first()->tag_id);
  117. }
  118. public function testCustomPivotClassUsingSync()
  119. {
  120. Carbon::setTestNow('2017-10-10 10:10:10');
  121. $post = Post::create(['title' => Str::random()]);
  122. $tag = TagWithCustomPivot::create(['name' => Str::random()]);
  123. $results = $post->tagsWithCustomPivot()->sync([
  124. $tag->id => ['flag' => 1],
  125. ]);
  126. $this->assertNotEmpty($results['attached']);
  127. $results = $post->tagsWithCustomPivot()->sync([
  128. $tag->id => ['flag' => 1],
  129. ]);
  130. $this->assertEmpty($results['updated']);
  131. $results = $post->tagsWithCustomPivot()->sync([]);
  132. $this->assertNotEmpty($results['detached']);
  133. }
  134. public function testCustomPivotClassUsingUpdateExistingPivot()
  135. {
  136. Carbon::setTestNow('2017-10-10 10:10:10');
  137. $post = Post::create(['title' => Str::random()]);
  138. $tag = TagWithCustomPivot::create(['name' => Str::random()]);
  139. DB::table('posts_tags')->insert([
  140. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'empty'],
  141. ]);
  142. // Test on actually existing pivot
  143. $this->assertEquals(
  144. 1,
  145. $post->tagsWithCustomExtraPivot()->updateExistingPivot($tag->id, ['flag' => 'exclude'])
  146. );
  147. foreach ($post->tagsWithCustomExtraPivot as $tag) {
  148. $this->assertSame('exclude', $tag->pivot->flag);
  149. }
  150. // Test on non-existent pivot
  151. $this->assertEquals(
  152. 0,
  153. $post->tagsWithCustomExtraPivot()->updateExistingPivot(0, ['flag' => 'exclude'])
  154. );
  155. }
  156. /** @group SkipMSSQL */
  157. public function testCustomPivotClassUpdatesTimestamps()
  158. {
  159. Carbon::setTestNow('2017-10-10 10:10:10');
  160. $post = Post::create(['title' => Str::random()]);
  161. $tag = TagWithCustomPivot::create(['name' => Str::random()]);
  162. DB::table('posts_tags')->insert([
  163. [
  164. 'post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'empty',
  165. 'created_at' => '2017-10-10 10:10:10',
  166. 'updated_at' => '2017-10-10 10:10:10',
  167. ],
  168. ]);
  169. Carbon::setTestNow('2017-10-10 10:10:20'); // +10 seconds
  170. $this->assertEquals(
  171. 1,
  172. $post->tagsWithCustomExtraPivot()->updateExistingPivot($tag->id, ['flag' => 'exclude'])
  173. );
  174. foreach ($post->tagsWithCustomExtraPivot as $tag) {
  175. $this->assertSame('exclude', $tag->pivot->flag);
  176. $this->assertEquals('2017-10-10 10:10:10', $tag->pivot->getAttributes()['created_at']);
  177. $this->assertEquals('2017-10-10 10:10:20', $tag->pivot->getAttributes()['updated_at']); // +10 seconds
  178. }
  179. }
  180. public function testAttachMethod()
  181. {
  182. $post = Post::create(['title' => Str::random()]);
  183. $tag = Tag::create(['name' => Str::random()]);
  184. $tag2 = Tag::create(['name' => Str::random()]);
  185. $tag3 = Tag::create(['name' => Str::random()]);
  186. $tag4 = Tag::create(['name' => Str::random()]);
  187. $tag5 = Tag::create(['name' => Str::random()]);
  188. $tag6 = Tag::create(['name' => Str::random()]);
  189. $tag7 = Tag::create(['name' => Str::random()]);
  190. $tag8 = Tag::create(['name' => Str::random()]);
  191. $post->tags()->attach($tag->id);
  192. $this->assertEquals($tag->name, $post->tags[0]->name);
  193. $this->assertNotNull($post->tags[0]->pivot->created_at);
  194. $post->tags()->attach($tag2->id, ['flag' => 'taylor']);
  195. $post->load('tags');
  196. $this->assertEquals($tag2->name, $post->tags[1]->name);
  197. $this->assertSame('taylor', $post->tags[1]->pivot->flag);
  198. $post->tags()->attach([$tag3->id, $tag4->id]);
  199. $post->load('tags');
  200. $this->assertEquals($tag3->name, $post->tags[2]->name);
  201. $this->assertEquals($tag4->name, $post->tags[3]->name);
  202. $post->tags()->attach([$tag5->id => ['flag' => 'mohamed'], $tag6->id => ['flag' => 'adam']]);
  203. $post->load('tags');
  204. $this->assertEquals($tag5->name, $post->tags[4]->name);
  205. $this->assertSame('mohamed', $post->tags[4]->pivot->flag);
  206. $this->assertEquals($tag6->name, $post->tags[5]->name);
  207. $this->assertSame('adam', $post->tags[5]->pivot->flag);
  208. $post->tags()->attach(new Collection([$tag7, $tag8]));
  209. $post->load('tags');
  210. $this->assertEquals($tag7->name, $post->tags[6]->name);
  211. $this->assertEquals($tag8->name, $post->tags[7]->name);
  212. }
  213. public function testDetachMethod()
  214. {
  215. $post = Post::create(['title' => Str::random()]);
  216. $tag = Tag::create(['name' => Str::random()]);
  217. $tag2 = Tag::create(['name' => Str::random()]);
  218. $tag3 = Tag::create(['name' => Str::random()]);
  219. $tag4 = Tag::create(['name' => Str::random()]);
  220. $tag5 = Tag::create(['name' => Str::random()]);
  221. Tag::create(['name' => Str::random()]);
  222. Tag::create(['name' => Str::random()]);
  223. $post->tags()->attach(Tag::all());
  224. $this->assertEquals(Tag::pluck('name'), $post->tags->pluck('name'));
  225. $post->tags()->detach($tag->id);
  226. $post->load('tags');
  227. $this->assertEquals(
  228. Tag::whereNotIn('id', [$tag->id])->pluck('name'),
  229. $post->tags->pluck('name')
  230. );
  231. $post->tags()->detach([$tag2->id, $tag3->id]);
  232. $post->load('tags');
  233. $this->assertEquals(
  234. Tag::whereNotIn('id', [$tag->id, $tag2->id, $tag3->id])->pluck('name'),
  235. $post->tags->pluck('name')
  236. );
  237. $post->tags()->detach(new Collection([$tag4, $tag5]));
  238. $post->load('tags');
  239. $this->assertEquals(
  240. Tag::whereNotIn('id', [$tag->id, $tag2->id, $tag3->id, $tag4->id, $tag5->id])->pluck('name'),
  241. $post->tags->pluck('name')
  242. );
  243. $this->assertCount(2, $post->tags);
  244. $post->tags()->detach();
  245. $post->load('tags');
  246. $this->assertCount(0, $post->tags);
  247. }
  248. public function testFirstMethod()
  249. {
  250. $post = Post::create(['title' => Str::random()]);
  251. $tag = Tag::create(['name' => Str::random()]);
  252. $post->tags()->attach(Tag::all());
  253. $this->assertEquals($tag->name, $post->tags()->first()->name);
  254. }
  255. public function testFirstOrFailMethod()
  256. {
  257. $this->expectException(ModelNotFoundException::class);
  258. $post = Post::create(['title' => Str::random()]);
  259. $post->tags()->firstOrFail(['id']);
  260. }
  261. public function testFindMethod()
  262. {
  263. $post = Post::create(['title' => Str::random()]);
  264. $tag = Tag::create(['name' => Str::random()]);
  265. $tag2 = Tag::create(['name' => Str::random()]);
  266. $post->tags()->attach(Tag::all());
  267. $this->assertEquals($tag2->name, $post->tags()->find($tag2->id)->name);
  268. $this->assertCount(0, $post->tags()->findMany([]));
  269. $this->assertCount(2, $post->tags()->findMany([$tag->id, $tag2->id]));
  270. $this->assertCount(0, $post->tags()->findMany(new Collection));
  271. $this->assertCount(2, $post->tags()->findMany(new Collection([$tag->id, $tag2->id])));
  272. }
  273. public function testFindOrFailMethod()
  274. {
  275. $this->expectException(ModelNotFoundException::class);
  276. $this->expectExceptionMessage('No query results for model [Illuminate\Tests\Integration\Database\EloquentBelongsToManyTest\Tag] 10');
  277. $post = Post::create(['title' => Str::random()]);
  278. Tag::create(['name' => Str::random()]);
  279. $post->tags()->attach(Tag::all());
  280. $post->tags()->findOrFail(10);
  281. }
  282. public function testFindOrFailMethodWithMany()
  283. {
  284. $this->expectException(ModelNotFoundException::class);
  285. $this->expectExceptionMessage('No query results for model [Illuminate\Tests\Integration\Database\EloquentBelongsToManyTest\Tag] 10, 11');
  286. $post = Post::create(['title' => Str::random()]);
  287. Tag::create(['name' => Str::random()]);
  288. $post->tags()->attach(Tag::all());
  289. $post->tags()->findOrFail([10, 11]);
  290. }
  291. public function testFindOrFailMethodWithManyUsingCollection()
  292. {
  293. $this->expectException(ModelNotFoundException::class);
  294. $this->expectExceptionMessage('No query results for model [Illuminate\Tests\Integration\Database\EloquentBelongsToManyTest\Tag] 10, 11');
  295. $post = Post::create(['title' => Str::random()]);
  296. Tag::create(['name' => Str::random()]);
  297. $post->tags()->attach(Tag::all());
  298. $post->tags()->findOrFail(new Collection([10, 11]));
  299. }
  300. public function testFindOrNewMethod()
  301. {
  302. $post = Post::create(['title' => Str::random()]);
  303. $tag = Tag::create(['name' => Str::random()]);
  304. $post->tags()->attach(Tag::all());
  305. $this->assertEquals($tag->id, $post->tags()->findOrNew($tag->id)->id);
  306. $this->assertNull($post->tags()->findOrNew(666)->id);
  307. $this->assertInstanceOf(Tag::class, $post->tags()->findOrNew(666));
  308. }
  309. public function testFirstOrNewMethod()
  310. {
  311. $post = Post::create(['title' => Str::random()]);
  312. $tag = Tag::create(['name' => Str::random()]);
  313. $post->tags()->attach(Tag::all());
  314. $this->assertEquals($tag->id, $post->tags()->firstOrNew(['id' => $tag->id])->id);
  315. $this->assertNull($post->tags()->firstOrNew(['id' => 666])->id);
  316. $this->assertInstanceOf(Tag::class, $post->tags()->firstOrNew(['id' => 666]));
  317. }
  318. public function testFirstOrCreateMethod()
  319. {
  320. $post = Post::create(['title' => Str::random()]);
  321. $tag = Tag::create(['name' => Str::random()]);
  322. $post->tags()->attach(Tag::all());
  323. $this->assertEquals($tag->id, $post->tags()->firstOrCreate(['name' => $tag->name])->id);
  324. $new = $post->tags()->firstOrCreate(['name' => 'wavez']);
  325. $this->assertSame('wavez', $new->name);
  326. $this->assertNotNull($new->id);
  327. }
  328. public function testUpdateOrCreateMethod()
  329. {
  330. $post = Post::create(['title' => Str::random()]);
  331. $tag = Tag::create(['name' => Str::random()]);
  332. $post->tags()->attach(Tag::all());
  333. $post->tags()->updateOrCreate(['id' => $tag->id], ['name' => 'wavez']);
  334. $this->assertSame('wavez', $tag->fresh()->name);
  335. $post->tags()->updateOrCreate(['id' => 666], ['name' => 'dives']);
  336. $this->assertNotNull($post->tags()->whereName('dives')->first());
  337. }
  338. public function testSyncMethod()
  339. {
  340. $post = Post::create(['title' => Str::random()]);
  341. $tag = Tag::create(['name' => Str::random()]);
  342. $tag2 = Tag::create(['name' => Str::random()]);
  343. $tag3 = Tag::create(['name' => Str::random()]);
  344. $tag4 = Tag::create(['name' => Str::random()]);
  345. $post->tags()->sync([$tag->id, $tag2->id]);
  346. $this->assertEquals(
  347. Tag::whereIn('id', [$tag->id, $tag2->id])->pluck('name'),
  348. $post->load('tags')->tags->pluck('name')
  349. );
  350. $output = $post->tags()->sync([$tag->id, $tag3->id, $tag4->id]);
  351. $this->assertEquals(
  352. Tag::whereIn('id', [$tag->id, $tag3->id, $tag4->id])->pluck('name'),
  353. $post->load('tags')->tags->pluck('name')
  354. );
  355. $this->assertEquals([
  356. 'attached' => [$tag3->id, $tag4->id],
  357. 'detached' => [1 => $tag2->id],
  358. 'updated' => [],
  359. ], $output);
  360. $post->tags()->sync([]);
  361. $this->assertEmpty($post->load('tags')->tags);
  362. $post->tags()->sync([
  363. $tag->id => ['flag' => 'taylor'],
  364. $tag2->id => ['flag' => 'mohamed'],
  365. ]);
  366. $post->load('tags');
  367. $this->assertEquals($tag->name, $post->tags[0]->name);
  368. $this->assertSame('taylor', $post->tags[0]->pivot->flag);
  369. $this->assertEquals($tag2->name, $post->tags[1]->name);
  370. $this->assertSame('mohamed', $post->tags[1]->pivot->flag);
  371. }
  372. public function testSyncWithoutDetachingMethod()
  373. {
  374. $post = Post::create(['title' => Str::random()]);
  375. $tag = Tag::create(['name' => Str::random()]);
  376. $tag2 = Tag::create(['name' => Str::random()]);
  377. $post->tags()->sync([$tag->id]);
  378. $this->assertEquals(
  379. Tag::whereIn('id', [$tag->id])->pluck('name'),
  380. $post->load('tags')->tags->pluck('name')
  381. );
  382. $post->tags()->syncWithoutDetaching([$tag2->id]);
  383. $this->assertEquals(
  384. Tag::whereIn('id', [$tag->id, $tag2->id])->pluck('name'),
  385. $post->load('tags')->tags->pluck('name')
  386. );
  387. }
  388. public function testToggleMethod()
  389. {
  390. $post = Post::create(['title' => Str::random()]);
  391. $tag = Tag::create(['name' => Str::random()]);
  392. $tag2 = Tag::create(['name' => Str::random()]);
  393. $post->tags()->toggle([$tag->id]);
  394. $this->assertEquals(
  395. Tag::whereIn('id', [$tag->id])->pluck('name'),
  396. $post->load('tags')->tags->pluck('name')
  397. );
  398. $post->tags()->toggle([$tag2->id, $tag->id]);
  399. $this->assertEquals(
  400. Tag::whereIn('id', [$tag2->id])->pluck('name'),
  401. $post->load('tags')->tags->pluck('name')
  402. );
  403. $post->tags()->toggle([$tag2->id, $tag->id => ['flag' => 'taylor']]);
  404. $post->load('tags');
  405. $this->assertEquals(
  406. Tag::whereIn('id', [$tag->id])->pluck('name'),
  407. $post->tags->pluck('name')
  408. );
  409. $this->assertSame('taylor', $post->tags[0]->pivot->flag);
  410. }
  411. public function testTouchingParent()
  412. {
  413. $post = Post::create(['title' => Str::random()]);
  414. $tag = TouchingTag::create(['name' => Str::random()]);
  415. $post->touchingTags()->attach([$tag->id]);
  416. $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  417. Carbon::setTestNow('2017-10-10 10:10:10');
  418. $tag->update(['name' => $tag->name]);
  419. $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  420. $tag->update(['name' => Str::random()]);
  421. $this->assertSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  422. }
  423. public function testTouchingRelatedModelsOnSync()
  424. {
  425. $tag = TouchingTag::create(['name' => Str::random()]);
  426. $post = Post::create(['title' => Str::random()]);
  427. $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  428. $this->assertNotSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString());
  429. Carbon::setTestNow('2017-10-10 10:10:10');
  430. $tag->posts()->sync([$post->id]);
  431. $this->assertSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  432. $this->assertSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString());
  433. }
  434. public function testNoTouchingHappensIfNotConfigured()
  435. {
  436. $tag = Tag::create(['name' => Str::random()]);
  437. $post = Post::create(['title' => Str::random()]);
  438. $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  439. $this->assertNotSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString());
  440. Carbon::setTestNow('2017-10-10 10:10:10');
  441. $tag->posts()->sync([$post->id]);
  442. $this->assertNotSame('2017-10-10 10:10:10', $post->fresh()->updated_at->toDateTimeString());
  443. $this->assertNotSame('2017-10-10 10:10:10', $tag->fresh()->updated_at->toDateTimeString());
  444. }
  445. /** @group SkipMSSQL */
  446. public function testCanRetrieveRelatedIds()
  447. {
  448. $post = Post::create(['title' => Str::random()]);
  449. DB::table('tags')->insert([
  450. ['id' => 200, 'name' => 'excluded'],
  451. ['id' => 300, 'name' => Str::random()],
  452. ]);
  453. DB::table('posts_tags')->insert([
  454. ['post_id' => $post->id, 'tag_id' => 200, 'flag' => ''],
  455. ['post_id' => $post->id, 'tag_id' => 300, 'flag' => 'exclude'],
  456. ['post_id' => $post->id, 'tag_id' => 400, 'flag' => ''],
  457. ]);
  458. $this->assertEquals([200, 400], $post->tags()->allRelatedIds()->toArray());
  459. }
  460. /** @group SkipMSSQL */
  461. public function testCanTouchRelatedModels()
  462. {
  463. $post = Post::create(['title' => Str::random()]);
  464. DB::table('tags')->insert([
  465. ['id' => 200, 'name' => Str::random()],
  466. ['id' => 300, 'name' => Str::random()],
  467. ]);
  468. DB::table('posts_tags')->insert([
  469. ['post_id' => $post->id, 'tag_id' => 200, 'flag' => ''],
  470. ['post_id' => $post->id, 'tag_id' => 300, 'flag' => 'exclude'],
  471. ['post_id' => $post->id, 'tag_id' => 400, 'flag' => ''],
  472. ]);
  473. Carbon::setTestNow('2017-10-10 10:10:10');
  474. $post->tags()->touch();
  475. foreach ($post->tags()->pluck('tags.updated_at') as $date) {
  476. $this->assertSame('2017-10-10 10:10:10', $date);
  477. }
  478. $this->assertNotSame('2017-10-10 10:10:10', Tag::find(300)->updated_at);
  479. }
  480. /** @group SkipMSSQL */
  481. public function testWherePivotOnString()
  482. {
  483. $tag = Tag::create(['name' => Str::random()]);
  484. $post = Post::create(['title' => Str::random()]);
  485. DB::table('posts_tags')->insert([
  486. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'foo'],
  487. ]);
  488. $relationTag = $post->tags()->wherePivot('flag', 'foo')->first();
  489. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  490. $relationTag = $post->tags()->wherePivot('flag', '=', 'foo')->first();
  491. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  492. }
  493. /** @group SkipMSSQL */
  494. public function testFirstWhere()
  495. {
  496. $tag = Tag::create(['name' => 'foo']);
  497. $post = Post::create(['title' => Str::random()]);
  498. DB::table('posts_tags')->insert([
  499. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'foo'],
  500. ]);
  501. $relationTag = $post->tags()->firstWhere('name', 'foo');
  502. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  503. $relationTag = $post->tags()->firstWhere('name', '=', 'foo');
  504. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  505. }
  506. /** @group SkipMSSQL */
  507. public function testWherePivotOnBoolean()
  508. {
  509. $tag = Tag::create(['name' => Str::random()]);
  510. $post = Post::create(['title' => Str::random()]);
  511. DB::table('posts_tags')->insert([
  512. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => true],
  513. ]);
  514. $relationTag = $post->tags()->wherePivot('flag', true)->first();
  515. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  516. $relationTag = $post->tags()->wherePivot('flag', '=', true)->first();
  517. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  518. }
  519. /** @group SkipMSSQL */
  520. public function testWherePivotInMethod()
  521. {
  522. $tag = Tag::create(['name' => Str::random()]);
  523. $post = Post::create(['title' => Str::random()]);
  524. DB::table('posts_tags')->insert([
  525. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'foo'],
  526. ]);
  527. $relationTag = $post->tags()->wherePivotIn('flag', ['foo'])->first();
  528. $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes());
  529. }
  530. public function testOrWherePivotInMethod()
  531. {
  532. $tag1 = Tag::create(['name' => Str::random()]);
  533. $tag2 = Tag::create(['name' => Str::random()]);
  534. $tag3 = Tag::create(['name' => Str::random()]);
  535. $post = Post::create(['title' => Str::random()]);
  536. DB::table('posts_tags')->insert([
  537. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo'],
  538. ]);
  539. DB::table('posts_tags')->insert([
  540. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => 'bar'],
  541. ]);
  542. DB::table('posts_tags')->insert([
  543. ['post_id' => $post->id, 'tag_id' => $tag3->id, 'flag' => 'baz'],
  544. ]);
  545. $relationTags = $post->tags()->wherePivotIn('flag', ['foo'])->orWherePivotIn('flag', ['baz'])->get();
  546. $this->assertEquals($relationTags->pluck('id')->toArray(), [$tag1->id, $tag3->id]);
  547. }
  548. /** @group SkipMSSQL */
  549. public function testWherePivotNotInMethod()
  550. {
  551. $tag1 = Tag::create(['name' => Str::random()]);
  552. $tag2 = Tag::create(['name' => Str::random()]);
  553. $post = Post::create(['title' => Str::random()]);
  554. DB::table('posts_tags')->insert([
  555. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo'],
  556. ]);
  557. DB::table('posts_tags')->insert([
  558. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => 'bar'],
  559. ]);
  560. $relationTag = $post->tags()->wherePivotNotIn('flag', ['foo'])->first();
  561. $this->assertEquals($relationTag->getAttributes(), $tag2->getAttributes());
  562. }
  563. public function testOrWherePivotNotInMethod()
  564. {
  565. $tag1 = Tag::create(['name' => Str::random()]);
  566. $tag2 = Tag::create(['name' => Str::random()]);
  567. $tag3 = Tag::create(['name' => Str::random()]);
  568. $post = Post::create(['title' => Str::random()]);
  569. DB::table('posts_tags')->insert([
  570. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo'],
  571. ]);
  572. DB::table('posts_tags')->insert([
  573. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => 'bar'],
  574. ]);
  575. DB::table('posts_tags')->insert([
  576. ['post_id' => $post->id, 'tag_id' => $tag3->id, 'flag' => 'baz'],
  577. ]);
  578. $relationTags = $post->tags()->wherePivotIn('flag', ['foo'])->orWherePivotNotIn('flag', ['baz'])->get();
  579. $this->assertEquals($relationTags->pluck('id')->toArray(), [$tag1->id, $tag2->id]);
  580. }
  581. /** @group SkipMSSQL */
  582. public function testWherePivotNullMethod()
  583. {
  584. $tag1 = Tag::create(['name' => Str::random()]);
  585. $tag2 = Tag::create(['name' => Str::random()]);
  586. $post = Post::create(['title' => Str::random()]);
  587. DB::table('posts_tags')->insert([
  588. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo'],
  589. ]);
  590. DB::table('posts_tags')->insert([
  591. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => null],
  592. ]);
  593. $relationTag = $post->tagsWithExtraPivot()->wherePivotNull('flag')->first();
  594. $this->assertEquals($relationTag->getAttributes(), $tag2->getAttributes());
  595. }
  596. /** @group SkipMSSQL */
  597. public function testWherePivotNotNullMethod()
  598. {
  599. $tag1 = Tag::create(['name' => Str::random()]);
  600. $tag2 = Tag::create(['name' => Str::random()]);
  601. $post = Post::create(['title' => Str::random()]);
  602. DB::table('posts_tags')->insert([
  603. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo'],
  604. ]);
  605. DB::table('posts_tags')->insert([
  606. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => null],
  607. ]);
  608. $relationTag = $post->tagsWithExtraPivot()->wherePivotNotNull('flag')->first();
  609. $this->assertEquals($relationTag->getAttributes(), $tag1->getAttributes());
  610. }
  611. public function testCanUpdateExistingPivot()
  612. {
  613. $tag = Tag::create(['name' => Str::random()]);
  614. $post = Post::create(['title' => Str::random()]);
  615. DB::table('posts_tags')->insert([
  616. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'empty'],
  617. ]);
  618. $post->tagsWithExtraPivot()->updateExistingPivot($tag->id, ['flag' => 'exclude']);
  619. foreach ($post->tagsWithExtraPivot as $tag) {
  620. $this->assertSame('exclude', $tag->pivot->flag);
  621. }
  622. }
  623. public function testCanUpdateExistingPivotUsingArrayableOfIds()
  624. {
  625. $tags = new Collection([
  626. $tag1 = Tag::create(['name' => Str::random()]),
  627. $tag2 = Tag::create(['name' => Str::random()]),
  628. ]);
  629. $post = Post::create(['title' => Str::random()]);
  630. DB::table('posts_tags')->insert([
  631. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'empty'],
  632. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => 'empty'],
  633. ]);
  634. $post->tagsWithExtraPivot()->updateExistingPivot($tags, ['flag' => 'exclude']);
  635. foreach ($post->tagsWithExtraPivot as $tag) {
  636. $this->assertSame('exclude', $tag->pivot->flag);
  637. }
  638. }
  639. public function testCanUpdateExistingPivotUsingModel()
  640. {
  641. $tag = Tag::create(['name' => Str::random()]);
  642. $post = Post::create(['title' => Str::random()]);
  643. DB::table('posts_tags')->insert([
  644. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'empty'],
  645. ]);
  646. $post->tagsWithExtraPivot()->updateExistingPivot($tag, ['flag' => 'exclude']);
  647. foreach ($post->tagsWithExtraPivot as $tag) {
  648. $this->assertSame('exclude', $tag->pivot->flag);
  649. }
  650. }
  651. public function testCustomRelatedKey()
  652. {
  653. $post = Post::create(['title' => Str::random()]);
  654. $tag = $post->tagsWithCustomRelatedKey()->create(['name' => Str::random()]);
  655. $this->assertEquals($tag->name, $post->tagsWithCustomRelatedKey()->first()->pivot->tag_name);
  656. $post->tagsWithCustomRelatedKey()->detach($tag);
  657. $post->tagsWithCustomRelatedKey()->attach($tag);
  658. $this->assertEquals($tag->name, $post->tagsWithCustomRelatedKey()->first()->pivot->tag_name);
  659. $post->tagsWithCustomRelatedKey()->detach(new Collection([$tag]));
  660. $post->tagsWithCustomRelatedKey()->attach(new Collection([$tag]));
  661. $this->assertEquals($tag->name, $post->tagsWithCustomRelatedKey()->first()->pivot->tag_name);
  662. $post->tagsWithCustomRelatedKey()->updateExistingPivot($tag, ['flag' => 'exclude']);
  663. $this->assertSame('exclude', $post->tagsWithCustomRelatedKey()->first()->pivot->flag);
  664. }
  665. public function testGlobalScopeColumns()
  666. {
  667. $tag = Tag::create(['name' => Str::random()]);
  668. $post = Post::create(['title' => Str::random()]);
  669. DB::table('posts_tags')->insert([
  670. ['post_id' => $post->id, 'tag_id' => $tag->id, 'flag' => 'empty'],
  671. ]);
  672. $tags = $post->tagsWithGlobalScope;
  673. $this->assertEquals(['id' => 1], $tags[0]->getAttributes());
  674. }
  675. public function testPivotDoesntHavePrimaryKey()
  676. {
  677. $user = User::create(['name' => Str::random()]);
  678. $post1 = Post::create(['title' => Str::random()]);
  679. $post2 = Post::create(['title' => Str::random()]);
  680. $user->postsWithCustomPivot()->sync([$post1->uuid]);
  681. $this->assertEquals($user->uuid, $user->postsWithCustomPivot()->first()->pivot->user_uuid);
  682. $this->assertEquals($post1->uuid, $user->postsWithCustomPivot()->first()->pivot->post_uuid);
  683. $this->assertEquals(1, $user->postsWithCustomPivot()->first()->pivot->is_draft);
  684. $user->postsWithCustomPivot()->sync([$post2->uuid]);
  685. $this->assertEquals($user->uuid, $user->postsWithCustomPivot()->first()->pivot->user_uuid);
  686. $this->assertEquals($post2->uuid, $user->postsWithCustomPivot()->first()->pivot->post_uuid);
  687. $this->assertEquals(1, $user->postsWithCustomPivot()->first()->pivot->is_draft);
  688. $user->postsWithCustomPivot()->updateExistingPivot($post2->uuid, ['is_draft' => 0]);
  689. $this->assertEquals(0, $user->postsWithCustomPivot()->first()->pivot->is_draft);
  690. }
  691. /** @group SkipMSSQL */
  692. public function testOrderByPivotMethod()
  693. {
  694. $tag1 = Tag::create(['name' => Str::random()]);
  695. $tag2 = Tag::create(['name' => Str::random()]);
  696. $tag3 = Tag::create(['name' => Str::random()]);
  697. $tag4 = Tag::create(['name' => Str::random()]);
  698. $post = Post::create(['title' => Str::random()]);
  699. DB::table('posts_tags')->insert([
  700. ['post_id' => $post->id, 'tag_id' => $tag1->id, 'flag' => 'foo3'],
  701. ['post_id' => $post->id, 'tag_id' => $tag2->id, 'flag' => 'foo1'],
  702. ['post_id' => $post->id, 'tag_id' => $tag3->id, 'flag' => 'foo4'],
  703. ['post_id' => $post->id, 'tag_id' => $tag4->id, 'flag' => 'foo2'],
  704. ]);
  705. $relationTag1 = $post->tagsWithCustomExtraPivot()->orderByPivot('flag', 'asc')->first();
  706. $this->assertEquals($relationTag1->getAttributes(), $tag2->getAttributes());
  707. $relationTag2 = $post->tagsWithCustomExtraPivot()->orderByPivot('flag', 'desc')->first();
  708. $this->assertEquals($relationTag2->getAttributes(), $tag3->getAttributes());
  709. }
  710. public function testFirstOrMethod()
  711. {
  712. $user1 = User::create(['name' => Str::random()]);
  713. $user2 = User::create(['name' => Str::random()]);
  714. $user3 = User::create(['name' => Str::random()]);
  715. $post1 = Post::create(['title' => Str::random()]);
  716. $post2 = Post::create(['title' => Str::random()]);
  717. $post3 = Post::create(['title' => Str::random()]);
  718. $user1->posts()->sync([$post1->uuid, $post2->uuid]);
  719. $user2->posts()->sync([$post1->uuid, $post2->uuid]);
  720. $this->assertEquals(
  721. $post1->id,
  722. $user2->posts()->firstOr(function () {
  723. return Post::create(['title' => Str::random()]);
  724. })->id
  725. );
  726. $this->assertEquals(
  727. $post3->id,
  728. $user3->posts()->firstOr(function () use ($post3) {
  729. return $post3;
  730. })->id
  731. );
  732. }
  733. }
  734. class User extends Model
  735. {
  736. public $table = 'users';
  737. public $timestamps = true;
  738. protected $guarded = [];
  739. protected static function boot()
  740. {
  741. parent::boot();
  742. static::creating(function ($model) {
  743. $model->setAttribute('uuid', Str::random());
  744. });
  745. }
  746. public function posts()
  747. {
  748. return $this->belongsToMany(Post::class, 'users_posts', 'user_uuid', 'post_uuid', 'uuid', 'uuid')
  749. ->withPivot('is_draft')
  750. ->withTimestamps();
  751. }
  752. public function postsWithCustomPivot()
  753. {
  754. return $this->belongsToMany(Post::class, 'users_posts', 'user_uuid', 'post_uuid', 'uuid', 'uuid')
  755. ->using(UserPostPivot::class)
  756. ->withPivot('is_draft')
  757. ->withTimestamps();
  758. }
  759. }
  760. class Post extends Model
  761. {
  762. public $table = 'posts';
  763. public $timestamps = true;
  764. protected $guarded = [];
  765. protected $touches = ['touchingTags'];
  766. protected static function boot()
  767. {
  768. parent::boot();
  769. static::creating(function ($model) {
  770. $model->setAttribute('uuid', Str::random());
  771. });
  772. }
  773. public function users()
  774. {
  775. return $this->belongsToMany(User::class, 'users_posts', 'post_uuid', 'user_uuid', 'uuid', 'uuid')
  776. ->withPivot('is_draft')
  777. ->withTimestamps();
  778. }
  779. public function tags()
  780. {
  781. return $this->belongsToMany(Tag::class, 'posts_tags', 'post_id', 'tag_id')
  782. ->withPivot('flag')
  783. ->withTimestamps()
  784. ->wherePivot('flag', '<>', 'exclude');
  785. }
  786. public function tagsWithExtraPivot()
  787. {
  788. return $this->belongsToMany(Tag::class, 'posts_tags', 'post_id', 'tag_id')
  789. ->withPivot('flag');
  790. }
  791. public function touchingTags()
  792. {
  793. return $this->belongsToMany(TouchingTag::class, 'posts_tags', 'post_id', 'tag_id')
  794. ->withTimestamps();
  795. }
  796. public function tagsWithCustomPivot()
  797. {
  798. return $this->belongsToMany(TagWithCustomPivot::class, 'posts_tags', 'post_id', 'tag_id')
  799. ->using(PostTagPivot::class)
  800. ->withTimestamps();
  801. }
  802. public function tagsWithCustomExtraPivot()
  803. {
  804. return $this->belongsToMany(TagWithCustomPivot::class, 'posts_tags', 'post_id', 'tag_id')
  805. ->using(PostTagPivot::class)
  806. ->withTimestamps()
  807. ->withPivot('flag');
  808. }
  809. public function tagsWithCustomPivotClass()
  810. {
  811. return $this->belongsToMany(TagWithCustomPivot::class, PostTagPivot::class, 'post_id', 'tag_id');
  812. }
  813. public function tagsWithCustomAccessor()
  814. {
  815. return $this->belongsToMany(TagWithCustomPivot::class, 'posts_tags', 'post_id', 'tag_id')
  816. ->using(PostTagPivot::class)
  817. ->as('tag');
  818. }
  819. public function tagsWithCustomRelatedKey()
  820. {
  821. return $this->belongsToMany(Tag::class, 'posts_tags', 'post_id', 'tag_name', 'id', 'name')
  822. ->withPivot('flag');
  823. }
  824. public function tagsWithGlobalScope()
  825. {
  826. return $this->belongsToMany(TagWithGlobalScope::class, 'posts_tags', 'post_id', 'tag_id');
  827. }
  828. }
  829. class Tag extends Model
  830. {
  831. public $table = 'tags';
  832. public $timestamps = true;
  833. protected $fillable = ['name'];
  834. public function posts()
  835. {
  836. return $this->belongsToMany(Post::class, 'posts_tags', 'tag_id', 'post_id');
  837. }
  838. }
  839. class TouchingTag extends Model
  840. {
  841. public $table = 'tags';
  842. public $timestamps = true;
  843. protected $guarded = [];
  844. protected $touches = ['posts'];
  845. public function posts()
  846. {
  847. return $this->belongsToMany(Post::class, 'posts_tags', 'tag_id', 'post_id');
  848. }
  849. }
  850. class TagWithCustomPivot extends Model
  851. {
  852. public $table = 'tags';
  853. public $timestamps = true;
  854. protected $guarded = [];
  855. public function posts()
  856. {
  857. return $this->belongsToMany(Post::class, 'posts_tags', 'tag_id', 'post_id');
  858. }
  859. }
  860. class UserPostPivot extends Pivot
  861. {
  862. protected $table = 'users_posts';
  863. }
  864. class PostTagPivot extends Pivot
  865. {
  866. protected $table = 'posts_tags';
  867. public function getCreatedAtAttribute($value)
  868. {
  869. return Carbon::parse($value)->format('U');
  870. }
  871. }
  872. class TagWithGlobalScope extends Model
  873. {
  874. public $table = 'tags';
  875. public $timestamps = true;
  876. protected $guarded = [];
  877. public static function boot()
  878. {
  879. parent::boot();
  880. static::addGlobalScope(function ($query) {
  881. $query->select('tags.id');
  882. });
  883. }
  884. }