Skip to content
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

Hybrid support for BelongsToMany relationship #2688

Merged
Merged
Show file tree
Hide file tree
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
Expand Up @@ -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

-
Expand Down
29 changes: 25 additions & 4 deletions src/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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());

Expand Down
51 changes: 51 additions & 0 deletions tests/HybridRelationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,6 +37,7 @@ public function tearDown(): void
SqlUser::truncate();
SqlBook::truncate();
SqlRole::truncate();
Skill::truncate();
}

public function testSqlRelations()
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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()]);
});
}
}
}
Loading