Skip to content

Commit 2adbf87

Browse files
Hybrid support for BelongsToMany relationship (#2688)
Co-authored-by: Junio Hyago <35033754+juniohyago@users.noreply.github.com>
1 parent 57060eb commit 2adbf87

File tree

5 files changed

+96
-5
lines changed

5 files changed

+96
-5
lines changed

phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ parameters:
22
ignoreErrors:
33
-
44
message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#"
5-
count: 3
5+
count: 2
66
path: src/Relations/BelongsToMany.php
77

88
-

src/Relations/BelongsToMany.php

+25-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use function array_values;
1818
use function assert;
1919
use function count;
20+
use function in_array;
2021
use function is_numeric;
2122

2223
class BelongsToMany extends EloquentBelongsToMany
@@ -124,7 +125,14 @@ public function sync($ids, $detaching = true)
124125
// First we need to attach any of the associated models that are not currently
125126
// in this joining table. We'll spin through the given IDs, checking to see
126127
// if they exist in the array of current ones, and if not we will insert.
127-
$current = $this->parent->{$this->relatedPivotKey} ?: [];
128+
$current = match ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) {
129+
true => $this->parent->{$this->relatedPivotKey} ?: [],
130+
false => $this->parent->{$this->relationName} ?: [],
131+
};
132+
133+
if ($current instanceof Collection) {
134+
$current = $this->parseIds($current);
135+
}
128136

129137
$records = $this->formatRecordsList($ids);
130138

@@ -193,7 +201,14 @@ public function attach($id, array $attributes = [], $touch = true)
193201
}
194202

195203
// Attach the new ids to the parent model.
196-
$this->parent->push($this->relatedPivotKey, (array) $id, true);
204+
if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) {
205+
$this->parent->push($this->relatedPivotKey, (array) $id, true);
206+
} else {
207+
$instance = new $this->related();
208+
$instance->forceFill([$this->relatedKey => $id]);
209+
$relationData = $this->parent->{$this->relationName}->push($instance)->unique($this->relatedKey);
210+
$this->parent->setRelation($this->relationName, $relationData);
211+
}
197212

198213
if (! $touch) {
199214
return;
@@ -217,15 +232,21 @@ public function detach($ids = [], $touch = true)
217232
$ids = (array) $ids;
218233

219234
// Detach all ids from the parent model.
220-
$this->parent->pull($this->relatedPivotKey, $ids);
235+
if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) {
236+
$this->parent->pull($this->relatedPivotKey, $ids);
237+
} else {
238+
$value = $this->parent->{$this->relationName}
239+
->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $ids));
240+
$this->parent->setRelation($this->relationName, $value);
241+
}
221242

222243
// Prepare the query to select all related objects.
223244
if (count($ids) > 0) {
224245
$query->whereIn($this->related->getKeyName(), $ids);
225246
}
226247

227248
// Remove the relation to the parent.
228-
assert($this->parent instanceof \MongoDB\Laravel\Eloquent\Model);
249+
assert($this->parent instanceof Model);
229250
assert($query instanceof \MongoDB\Laravel\Eloquent\Builder);
230251
$query->pull($this->foreignPivotKey, $this->parent->getKey());
231252

tests/HybridRelationsTest.php

+51
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Facades\DB;
99
use MongoDB\Laravel\Tests\Models\Book;
1010
use MongoDB\Laravel\Tests\Models\Role;
11+
use MongoDB\Laravel\Tests\Models\Skill;
1112
use MongoDB\Laravel\Tests\Models\SqlBook;
1213
use MongoDB\Laravel\Tests\Models\SqlRole;
1314
use MongoDB\Laravel\Tests\Models\SqlUser;
@@ -36,6 +37,7 @@ public function tearDown(): void
3637
SqlUser::truncate();
3738
SqlBook::truncate();
3839
SqlRole::truncate();
40+
Skill::truncate();
3941
}
4042

4143
public function testSqlRelations()
@@ -210,4 +212,53 @@ public function testHybridWith()
210212
$this->assertEquals($user->id, $user->books->count());
211213
});
212214
}
215+
216+
public function testHybridBelongsToMany()
217+
{
218+
$user = new SqlUser();
219+
$user2 = new SqlUser();
220+
$this->assertInstanceOf(SqlUser::class, $user);
221+
$this->assertInstanceOf(SQLiteConnection::class, $user->getConnection());
222+
$this->assertInstanceOf(SqlUser::class, $user2);
223+
$this->assertInstanceOf(SQLiteConnection::class, $user2->getConnection());
224+
225+
// Create Mysql Users
226+
$user->fill(['name' => 'John Doe'])->save();
227+
$user = SqlUser::query()->find($user->id);
228+
229+
$user2->fill(['name' => 'Maria Doe'])->save();
230+
$user2 = SqlUser::query()->find($user2->id);
231+
232+
// Create Mongodb Skills
233+
$skill = Skill::query()->create(['name' => 'Laravel']);
234+
$skill2 = Skill::query()->create(['name' => 'MongoDB']);
235+
236+
// sync (pivot is empty)
237+
$skill->sqlUsers()->sync([$user->id, $user2->id]);
238+
$check = Skill::query()->find($skill->_id);
239+
$this->assertEquals(2, $check->sqlUsers->count());
240+
241+
// sync (pivot is not empty)
242+
$skill->sqlUsers()->sync($user);
243+
$check = Skill::query()->find($skill->_id);
244+
$this->assertEquals(1, $check->sqlUsers->count());
245+
246+
// Inverse sync (pivot is empty)
247+
$user->skills()->sync([$skill->_id, $skill2->_id]);
248+
$check = SqlUser::find($user->id);
249+
$this->assertEquals(2, $check->skills->count());
250+
251+
// Inverse sync (pivot is not empty)
252+
$user->skills()->sync($skill);
253+
$check = SqlUser::find($user->id);
254+
$this->assertEquals(1, $check->skills->count());
255+
256+
// Inverse attach
257+
$user->skills()->sync([]);
258+
$check = SqlUser::find($user->id);
259+
$this->assertEquals(0, $check->skills->count());
260+
$user->skills()->attach($skill);
261+
$check = SqlUser::find($user->id);
262+
$this->assertEquals(1, $check->skills->count());
263+
}
213264
}

tests/Models/Skill.php

+6
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44

55
namespace MongoDB\Laravel\Tests\Models;
66

7+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
78
use MongoDB\Laravel\Eloquent\Model as Eloquent;
89

910
class Skill extends Eloquent
1011
{
1112
protected $connection = 'mongodb';
1213
protected $collection = 'skills';
1314
protected static $unguarded = true;
15+
16+
public function sqlUsers(): BelongsToMany
17+
{
18+
return $this->belongsToMany(SqlUser::class);
19+
}
1420
}

tests/Models/SqlUser.php

+13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace MongoDB\Laravel\Tests\Models;
66

77
use Illuminate\Database\Eloquent\Model as EloquentModel;
8+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
89
use Illuminate\Database\Eloquent\Relations\HasMany;
910
use Illuminate\Database\Eloquent\Relations\HasOne;
1011
use Illuminate\Database\Schema\Blueprint;
@@ -32,6 +33,11 @@ public function role(): HasOne
3233
return $this->hasOne(Role::class);
3334
}
3435

36+
public function skills(): BelongsToMany
37+
{
38+
return $this->belongsToMany(Skill::class, relatedPivotKey: 'skills');
39+
}
40+
3541
public function sqlBooks(): HasMany
3642
{
3743
return $this->hasMany(SqlBook::class);
@@ -51,5 +57,12 @@ public static function executeSchema(): void
5157
$table->string('name');
5258
$table->timestamps();
5359
});
60+
if (! $schema->hasTable('skill_sql_user')) {
61+
$schema->create('skill_sql_user', function (Blueprint $table) {
62+
$table->foreignIdFor(self::class)->constrained()->cascadeOnDelete();
63+
$table->string((new Skill())->getForeignKey());
64+
$table->primary([(new self())->getForeignKey(), (new Skill())->getForeignKey()]);
65+
});
66+
}
5467
}
5568
}

0 commit comments

Comments
 (0)