Skip to content

Hybrid support for BelongsToMany relationship #2688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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

-
29 changes: 25 additions & 4 deletions src/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
@@ -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
@@ -124,7 +125,14 @@ 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} ?: [];
$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);
}

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

@@ -193,7 +201,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;
@@ -217,15 +232,21 @@ public function detach($ids = [], $touch = true)
$ids = (array) $ids;

// Detach all ids from the parent model.
$this->parent->pull($this->relatedPivotKey, $ids);
if ($this->parent instanceof \MongoDB\Laravel\Eloquent\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.
if (count($ids) > 0) {
$query->whereIn($this->related->getKeyName(), $ids);
}

// 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());

51 changes: 51 additions & 0 deletions tests/HybridRelationsTest.php
Original file line number Diff line number Diff line change
@@ -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,53 @@ public function testHybridWith()
$this->assertEquals($user->id, $user->books->count());
});
}

public function testHybridBelongsToMany()
{
$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 Skills
$skill = Skill::query()->create(['name' => 'Laravel']);
$skill2 = Skill::query()->create(['name' => 'MongoDB']);

// sync (pivot is empty)
$skill->sqlUsers()->sync([$user->id, $user2->id]);
$check = Skill::query()->find($skill->_id);
$this->assertEquals(2, $check->sqlUsers->count());

// sync (pivot is not empty)
$skill->sqlUsers()->sync($user);
$check = Skill::query()->find($skill->_id);
$this->assertEquals(1, $check->sqlUsers->count());

// Inverse sync (pivot is empty)
$user->skills()->sync([$skill->_id, $skill2->_id]);
$check = SqlUser::find($user->id);
$this->assertEquals(2, $check->skills->count());

// Inverse sync (pivot is not empty)
$user->skills()->sync($skill);
$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());
}
}
6 changes: 6 additions & 0 deletions tests/Models/Skill.php
Original file line number Diff line number Diff line change
@@ -4,11 +4,17 @@

namespace MongoDB\Laravel\Tests\Models;

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use MongoDB\Laravel\Eloquent\Model as Eloquent;

class Skill extends Eloquent
{
protected $connection = 'mongodb';
protected $collection = 'skills';
protected static $unguarded = true;

public function sqlUsers(): BelongsToMany
{
return $this->belongsToMany(SqlUser::class);
}
}
13 changes: 13 additions & 0 deletions tests/Models/SqlUser.php
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to have a pivot table here? If the relations are already stored in the MongoDB document, it should not be duplicated in an SQL table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Check if it is a relation with an original model.
if (! is_subclass_of($related, MongoDBModel::class)) {
return parent::belongsToMany(
$related,
$collection,
$foreignPivotKey,
$relatedPivotKey,
$parentKey,
$relatedKey,
$relation,
);
}

The sql models don't reach our BelongsToMany class. So, we can't control this behavior.
As I know, the sql models, store their relations data in a pivot tabel and mongo models, store theirs in a pivot column.

$table->foreignIdFor(self::class)->constrained()->cascadeOnDelete();
$table->string((new Skill())->getForeignKey());
$table->primary([(new self())->getForeignKey(), (new Skill())->getForeignKey()]);
});
}
}
}