From bf1a0ed8a9aeda9d65e89667f91fb42462fb7a9b Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Mon, 27 Nov 2023 14:27:08 +0330 Subject: [PATCH 1/8] Add support for hybrid relationships; Co-Authored-By: Junio Hyago <35033754+juniohyago@users.noreply.github.com> --- src/Relations/BelongsToMany.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Relations/BelongsToMany.php b/src/Relations/BelongsToMany.php index 1d6b84ba8..91ef73eae 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -125,6 +125,10 @@ public function sync($ids, $detaching = true) // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. $current = $this->parent->{$this->relatedPivotKey} ?: []; + // Hybrid relationships support + if ($current instanceof Collection) { + $current = $this->parseIds($current); + } $records = $this->formatRecordsList($ids); @@ -216,8 +220,10 @@ public function detach($ids = [], $touch = true) // We'll return the numbers of affected rows when we do the deletes. $ids = (array) $ids; - // Detach all ids from the parent model. - $this->parent->pull($this->relatedPivotKey, $ids); + if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { + // Detach all ids from the parent model. + $this->parent->pull($this->relatedPivotKey, $ids); + } // Prepare the query to select all related objects. if (count($ids) > 0) { @@ -225,7 +231,7 @@ public function detach($ids = [], $touch = true) } // Remove the relation to the parent. - assert($this->parent instanceof \MongoDB\Laravel\Eloquent\Model); + assert($this->parent instanceof Model); assert($query instanceof \MongoDB\Laravel\Eloquent\Builder); $query->pull($this->foreignPivotKey, $this->parent->getKey()); From bc5636dd79fc0a6a5273312ef5b0fe6b4c7e067b Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Mon, 27 Nov 2023 14:27:46 +0330 Subject: [PATCH 2/8] Add test for hybrid BelongsToMany hybrid relationship; Co-Authored-By: Junio Hyago <35033754+juniohyago@users.noreply.github.com> --- tests/HybridRelationsTest.php | 43 +++++++++++++++++++++++++++++++++++ tests/Models/Skill.php | 6 +++++ tests/Models/SqlUser.php | 13 +++++++++++ 3 files changed, 62 insertions(+) diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 9ff6264e5..e41b1edf6 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\DB; use MongoDB\Laravel\Tests\Models\Book; use MongoDB\Laravel\Tests\Models\Role; +use MongoDB\Laravel\Tests\Models\Skill; use MongoDB\Laravel\Tests\Models\SqlBook; use MongoDB\Laravel\Tests\Models\SqlRole; use MongoDB\Laravel\Tests\Models\SqlUser; @@ -36,6 +37,7 @@ public function tearDown(): void SqlUser::truncate(); SqlBook::truncate(); SqlRole::truncate(); + Skill::truncate(); } public function testSqlRelations() @@ -210,4 +212,45 @@ public function testHybridWith() $this->assertEquals($user->id, $user->books->count()); }); } + + public function testHybridSync() + { + $user = new SqlUser(); + $user2 = new SqlUser; + $this->assertInstanceOf(SqlUser::class, $user); + $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection()); + $this->assertInstanceOf(SqlUser::class, $user2); + $this->assertInstanceOf(SQLiteConnection::class, $user2->getConnection()); + + // Create Mysql Users + $user->fill(['name' => 'John Doe'])->save(); + $user = SqlUser::query()->find($user->id); + + $user2->fill(['name' => 'Maria Doe'])->save(); + $user2 = SqlUser::query()->find($user2->id); + + // Create Mongodb Clients + $skill = Skill::query()->create(['name' => 'Laravel']); + $skill2 = Skill::query()->create(['name' => 'MongoDB']); + + // sync (pivot is empty) + $skill->sqlUsers()->sync([$user->id, $user2->id]); + $skill = Skill::query()->find($skill->_id); + $this->assertEquals(2, $skill->sqlUsers->count()); + + // sync (pivot is not empty) + $skill->sqlUsers()->sync($user); + $skill = Skill::query()->find($skill->_id); + $this->assertEquals(1, $skill->sqlUsers->count()); + + // sync (pivot is empty) + $user->skills()->sync([$skill->_id, $skill2->_id]); + $user = SqlUser::find($user->id); + $this->assertEquals(2, $user->skills->count()); + + // sync (pivot is not empty) + $user->skills()->sync($skill); + $user = SqlUser::find($user->id); + $this->assertEquals(1, $user->skills->count()); + } } diff --git a/tests/Models/Skill.php b/tests/Models/Skill.php index c4c1dbd0a..3b9a434ee 100644 --- a/tests/Models/Skill.php +++ b/tests/Models/Skill.php @@ -4,6 +4,7 @@ namespace MongoDB\Laravel\Tests\Models; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use MongoDB\Laravel\Eloquent\Model as Eloquent; class Skill extends Eloquent @@ -11,4 +12,9 @@ class Skill extends Eloquent protected $connection = 'mongodb'; protected $collection = 'skills'; protected static $unguarded = true; + + public function sqlUsers(): BelongsToMany + { + return $this->belongsToMany(SqlUser::class); + } } diff --git a/tests/Models/SqlUser.php b/tests/Models/SqlUser.php index 1fe11276a..34c65f42e 100644 --- a/tests/Models/SqlUser.php +++ b/tests/Models/SqlUser.php @@ -5,6 +5,7 @@ namespace MongoDB\Laravel\Tests\Models; use Illuminate\Database\Eloquent\Model as EloquentModel; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Schema\Blueprint; @@ -32,6 +33,11 @@ public function role(): HasOne return $this->hasOne(Role::class); } + public function skills(): BelongsToMany + { + return $this->belongsToMany(Skill::class, relatedPivotKey: 'skills'); + } + public function sqlBooks(): HasMany { return $this->hasMany(SqlBook::class); @@ -51,5 +57,12 @@ public static function executeSchema(): void $table->string('name'); $table->timestamps(); }); + if (! $schema->hasTable('skill_sql_user')) { + $schema->create('skill_sql_user', function (Blueprint $table) { + $table->foreignIdFor(self::class)->constrained()->cascadeOnDelete(); + $table->string((new Skill())->getForeignKey()); + $table->primary([(new self())->getForeignKey(), (new Skill())->getForeignKey()]); + }); + } } } From 3f9ea17fa2f231770d75f497ca752309387c7f56 Mon Sep 17 00:00:00 2001 From: hans-thomas Date: Mon, 27 Nov 2023 10:58:36 +0000 Subject: [PATCH 3/8] apply phpcbf formatting --- tests/HybridRelationsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index e41b1edf6..697c6d2d4 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -216,7 +216,7 @@ public function testHybridWith() public function testHybridSync() { $user = new SqlUser(); - $user2 = new SqlUser; + $user2 = new SqlUser(); $this->assertInstanceOf(SqlUser::class, $user); $this->assertInstanceOf(SQLiteConnection::class, $user->getConnection()); $this->assertInstanceOf(SqlUser::class, $user2); From eafa5ff6a61ccd9e7c438c63e794517cea3427fb Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Mon, 27 Nov 2023 16:38:45 +0330 Subject: [PATCH 4/8] WIP --- tests/HybridRelationsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 697c6d2d4..25b8673b7 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -213,7 +213,7 @@ public function testHybridWith() }); } - public function testHybridSync() + public function testHybridBelongsToMany() { $user = new SqlUser(); $user2 = new SqlUser(); From 55a5dff1b6e2da5c850a566641cd54baea3c299f Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Mon, 27 Nov 2023 17:14:52 +0330 Subject: [PATCH 5/8] WIP --- tests/HybridRelationsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 25b8673b7..8c3ebdf02 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -229,7 +229,7 @@ public function testHybridBelongsToMany() $user2->fill(['name' => 'Maria Doe'])->save(); $user2 = SqlUser::query()->find($user2->id); - // Create Mongodb Clients + // Create Mongodb Skills $skill = Skill::query()->create(['name' => 'Laravel']); $skill2 = Skill::query()->create(['name' => 'MongoDB']); From 0d727aae75cb0d39279a392fc460da49fda6c3c2 Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Wed, 29 Nov 2023 15:02:09 +0330 Subject: [PATCH 6/8] Update sql model instance; --- src/Relations/BelongsToMany.php | 22 ++++++++++++++++++---- tests/HybridRelationsTest.php | 28 ++++++++++++++++++---------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Relations/BelongsToMany.php b/src/Relations/BelongsToMany.php index 91ef73eae..92d68df1c 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -124,8 +124,11 @@ public function sync($ids, $detaching = true) // First we need to attach any of the associated models that are not currently // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. - $current = $this->parent->{$this->relatedPivotKey} ?: []; - // Hybrid relationships support + $current = match ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { + true => $this->parent->{$this->relatedPivotKey} ?: [], + false => $this->parent->{$this->relationName} ?: [], + }; + if ($current instanceof Collection) { $current = $this->parseIds($current); } @@ -197,7 +200,14 @@ public function attach($id, array $attributes = [], $touch = true) } // Attach the new ids to the parent model. - $this->parent->push($this->relatedPivotKey, (array) $id, true); + if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { + $this->parent->push($this->relatedPivotKey, (array) $id, true); + }else{ + $instance = new $this->related(); + $instance->forceFill([$this->relatedKey => $id]); + $relationData = $this->parent->{$this->relationName}->push($instance)->unique($this->relatedKey); + $this->parent->setRelation($this->relationName, $relationData); + } if (! $touch) { return; @@ -220,9 +230,13 @@ public function detach($ids = [], $touch = true) // We'll return the numbers of affected rows when we do the deletes. $ids = (array) $ids; + // Detach all ids from the parent model. if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { - // Detach all ids from the parent model. $this->parent->pull($this->relatedPivotKey, $ids); + }else{ + $value = $this->parent->{$this->relationName} + ->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $ids)); + $this->parent->setRelation($this->relationName, $value); } // Prepare the query to select all related objects. diff --git a/tests/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 8c3ebdf02..0080a3a47 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -235,22 +235,30 @@ public function testHybridBelongsToMany() // sync (pivot is empty) $skill->sqlUsers()->sync([$user->id, $user2->id]); - $skill = Skill::query()->find($skill->_id); - $this->assertEquals(2, $skill->sqlUsers->count()); + $check = Skill::query()->find($skill->_id); + $this->assertEquals(2, $check->sqlUsers->count()); // sync (pivot is not empty) $skill->sqlUsers()->sync($user); - $skill = Skill::query()->find($skill->_id); - $this->assertEquals(1, $skill->sqlUsers->count()); + $check = Skill::query()->find($skill->_id); + $this->assertEquals(1, $check->sqlUsers->count()); - // sync (pivot is empty) + // Inverse sync (pivot is empty) $user->skills()->sync([$skill->_id, $skill2->_id]); - $user = SqlUser::find($user->id); - $this->assertEquals(2, $user->skills->count()); + $check = SqlUser::find($user->id); + $this->assertEquals(2, $check->skills->count()); - // sync (pivot is not empty) + // Inverse sync (pivot is not empty) $user->skills()->sync($skill); - $user = SqlUser::find($user->id); - $this->assertEquals(1, $user->skills->count()); + $check = SqlUser::find($user->id); + $this->assertEquals(1, $check->skills->count()); + + // Inverse attach + $user->skills()->sync([]); + $check = SqlUser::find($user->id); + $this->assertEquals(0, $check->skills->count()); + $user->skills()->attach($skill); + $check = SqlUser::find($user->id); + $this->assertEquals(1, $check->skills->count()); } } From bf3c33da546e876f7c98bc4a5808e50182949312 Mon Sep 17 00:00:00 2001 From: hans-thomas Date: Wed, 29 Nov 2023 11:32:45 +0000 Subject: [PATCH 7/8] apply phpcbf formatting --- src/Relations/BelongsToMany.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Relations/BelongsToMany.php b/src/Relations/BelongsToMany.php index 92d68df1c..082f95e06 100644 --- a/src/Relations/BelongsToMany.php +++ b/src/Relations/BelongsToMany.php @@ -17,6 +17,7 @@ use function array_values; use function assert; use function count; +use function in_array; use function is_numeric; class BelongsToMany extends EloquentBelongsToMany @@ -200,9 +201,9 @@ public function attach($id, array $attributes = [], $touch = true) } // Attach the new ids to the parent model. - if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { + if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { $this->parent->push($this->relatedPivotKey, (array) $id, true); - }else{ + } else { $instance = new $this->related(); $instance->forceFill([$this->relatedKey => $id]); $relationData = $this->parent->{$this->relationName}->push($instance)->unique($this->relatedKey); @@ -233,7 +234,7 @@ public function detach($ids = [], $touch = true) // Detach all ids from the parent model. if ($this->parent instanceof \MongoDB\Laravel\Eloquent\Model) { $this->parent->pull($this->relatedPivotKey, $ids); - }else{ + } else { $value = $this->parent->{$this->relationName} ->filter(fn ($rel) => ! in_array($rel->{$this->relatedKey}, $ids)); $this->parent->setRelation($this->relationName, $value); From 64fca124c0ff2d7f23b76a0f6771a163f1a7acfa Mon Sep 17 00:00:00 2001 From: Mohammad Mortazavi Date: Wed, 29 Nov 2023 15:05:49 +0330 Subject: [PATCH 8/8] Update phpstan-baseline.neon --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 71a44a395..4869c6ca0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2,7 +2,7 @@ parameters: ignoreErrors: - message: "#^Method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:push\\(\\) invoked with 3 parameters, 0 required\\.$#" - count: 3 + count: 2 path: src/Relations/BelongsToMany.php -