diff --git a/src/Eloquent/HybridRelations.php b/src/Eloquent/HybridRelations.php index f0824c9fb..9551a6c43 100644 --- a/src/Eloquent/HybridRelations.php +++ b/src/Eloquent/HybridRelations.php @@ -15,11 +15,16 @@ use MongoDB\Laravel\Relations\HasOne; use MongoDB\Laravel\Relations\MorphMany; use MongoDB\Laravel\Relations\MorphTo; +use MongoDB\Laravel\Relations\MorphToMany; +use function array_pop; use function debug_backtrace; +use function implode; use function is_subclass_of; +use function preg_split; use const DEBUG_BACKTRACE_IGNORE_ARGS; +use const PREG_SPLIT_DELIM_CAPTURE; /** * Cross-database relationships between SQL and MongoDB. @@ -328,6 +333,125 @@ public function belongsToMany( ); } + /** + * Define a morph-to-many relationship. + * + * @param string $related + * @param string $name + * @param null $table + * @param null $foreignPivotKey + * @param null $relatedPivotKey + * @param null $parentKey + * @param null $relatedKey + * @param null $relation + * @param bool $inverse + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany + */ + public function morphToMany( + $related, + $name, + $table = null, + $foreignPivotKey = null, + $relatedPivotKey = null, + $parentKey = null, + $relatedKey = null, + $relation = null, + $inverse = false, + ) { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if ($relation === null) { + $relation = $this->guessBelongsToManyRelation(); + } + + // Check if it is a relation with an original model. + if (! is_subclass_of($related, Model::class)) { + return parent::morphToMany( + $related, + $name, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey, + $relatedKey, + $relation, + $inverse, + ); + } + + $instance = new $related(); + + $foreignPivotKey = $foreignPivotKey ?: $name . '_id'; + $relatedPivotKey = $relatedPivotKey ?: Str::plural($instance->getForeignKey()); + + // Now we're ready to create a new query builder for the related model and + // the relationship instances for this relation. This relation will set + // appropriate query constraints then entirely manage the hydration. + if (! $table) { + $words = preg_split('/(_)/u', $name, -1, PREG_SPLIT_DELIM_CAPTURE); + $lastWord = array_pop($words); + $table = implode('', $words) . Str::plural($lastWord); + } + + return new MorphToMany( + $instance->newQuery(), + $this, + $name, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey ?: $this->getKeyName(), + $relatedKey ?: $instance->getKeyName(), + $relation, + $inverse, + ); + } + + /** + * Define a polymorphic, inverse many-to-many relationship. + * + * @param string $related + * @param string $name + * @param null $table + * @param null $foreignPivotKey + * @param null $relatedPivotKey + * @param null $parentKey + * @param null $relatedKey + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany + */ + public function morphedByMany( + $related, + $name, + $table = null, + $foreignPivotKey = null, + $relatedPivotKey = null, + $parentKey = null, + $relatedKey = null, + $relation = null, + ) { + $foreignPivotKey = $foreignPivotKey ?: Str::plural($this->getForeignKey()); + + // For the inverse of the polymorphic many-to-many relations, we will change + // the way we determine the foreign and other keys, as it is the opposite + // of the morph-to-many method since we're figuring out these inverses. + $relatedPivotKey = $relatedPivotKey ?: $name . '_id'; + + return $this->morphToMany( + $related, + $name, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey, + $relatedKey, + $relatedKey, + true, + ); + } + /** @inheritdoc */ public function newEloquentBuilder($query) { diff --git a/src/Helpers/QueriesRelationships.php b/src/Helpers/QueriesRelationships.php index a83c96e3e..b1234124b 100644 --- a/src/Helpers/QueriesRelationships.php +++ b/src/Helpers/QueriesRelationships.php @@ -13,12 +13,15 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Collection; use MongoDB\Laravel\Eloquent\Model; +use MongoDB\Laravel\Relations\MorphToMany; use function array_count_values; use function array_filter; use function array_keys; use function array_map; use function class_basename; +use function collect; +use function get_class; use function in_array; use function is_array; use function is_string; @@ -114,13 +117,48 @@ public function addHybridHas(Relation $relation, $operator = '>=', $count = 1, $ $not = ! $not; } - $relations = $hasQuery->pluck($this->getHasCompareKey($relation)); + $relations = match (true) { + $relation instanceof MorphToMany => $relation->getInverse() ? + $this->handleMorphedByMany($hasQuery, $relation) : + $this->handleMorphToMany($hasQuery, $relation), + default => $hasQuery->pluck($this->getHasCompareKey($relation)) + }; $relatedIds = $this->getConstrainedRelatedIds($relations, $operator, $count); return $this->whereIn($this->getRelatedConstraintKey($relation), $relatedIds, $boolean, $not); } + /** + * @param Builder $hasQuery + * @param Relation $relation + * + * @return Collection + */ + private function handleMorphToMany($hasQuery, $relation) + { + // First we select the parent models that have a relation to our related model, + // Then extracts related model's ids from the pivot column + $hasQuery->where($relation->getTable() . '.' . $relation->getMorphType(), get_class($relation->getParent())); + $relations = $hasQuery->pluck($relation->getTable()); + $relations = $relation->extractIds($relations->flatten(1)->toArray(), $relation->getForeignPivotKeyName()); + + return collect($relations); + } + + /** + * @param Builder $hasQuery + * @param Relation $relation + * + * @return Collection + */ + private function handleMorphedByMany($hasQuery, $relation) + { + $hasQuery->whereNotNull($relation->getForeignPivotKeyName()); + + return $hasQuery->pluck($relation->getForeignPivotKeyName())->flatten(1); + } + /** @return string */ protected function getHasCompareKey(Relation $relation) { diff --git a/src/Relations/MorphToMany.php b/src/Relations/MorphToMany.php new file mode 100644 index 000000000..9c9576d90 --- /dev/null +++ b/src/Relations/MorphToMany.php @@ -0,0 +1,397 @@ +setWhere(); + } + } + + /** @inheritdoc */ + public function addEagerConstraints(array $models) + { + // To load relation's data, we act normally on MorphToMany relation, + // But on MorphedByMany relation, we collect related ids from pivot column + // and add to a whereIn condition + if ($this->getInverse()) { + $ids = $this->getKeys($models, $this->table); + $ids = $this->extractIds($ids[0] ?? []); + $this->query->whereIn($this->relatedKey, $ids); + } else { + parent::addEagerConstraints($models); + + $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + } + } + + /** + * Set the where clause for the relation query. + * + * @return $this + */ + protected function setWhere() + { + if ($this->getInverse()) { + $ids = $this->extractIds((array) $this->parent->{$this->table}); + + $this->query->whereIn($this->relatedKey, $ids); + } else { + $this->query->whereIn($this->relatedKey, (array) $this->parent->{$this->relatedPivotKey}); + } + + return $this; + } + + /** @inheritdoc */ + public function save(Model $model, array $joining = [], $touch = true) + { + $model->save(['touch' => false]); + + $this->attach($model, $joining, $touch); + + return $model; + } + + /** @inheritdoc */ + public function create(array $attributes = [], array $joining = [], $touch = true) + { + $instance = $this->related->newInstance($attributes); + + // Once we save the related model, we need to attach it to the base model via + // through intermediate table so we'll use the existing "attach" method to + // accomplish this which will insert the record and any more attributes. + $instance->save(['touch' => false]); + + $this->attach($instance, $joining, $touch); + + return $instance; + } + + /** @inheritdoc */ + public function sync($ids, $detaching = true) + { + $changes = [ + 'attached' => [], + 'detached' => [], + 'updated' => [], + ]; + + if ($ids instanceof Collection) { + $ids = $this->parseIds($ids); + } elseif ($ids instanceof Model) { + $ids = $this->parseIds($ids); + } + + // 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. + if ($this->getInverse()) { + $current = $this->extractIds($this->parent->{$this->table} ?: []); + } else { + $current = $this->parent->{$this->relatedPivotKey} ?: []; + } + + // See issue #256. + if ($current instanceof Collection) { + $current = $this->parseIds($current); + } + + $records = $this->formatRecordsList($ids); + + $current = Arr::wrap($current); + + $detach = array_diff($current, array_keys($records)); + + // We need to make sure we pass a clean array, so that it is not interpreted + // as an associative array. + $detach = array_values($detach); + + // Next, we will take the differences of the currents and given IDs and detach + // all of the entities that exist in the "current" array but are not in the + // the array of the IDs given to the method which will complete the sync. + if ($detaching && count($detach) > 0) { + $this->detach($detach); + + $changes['detached'] = array_map(function ($v) { + return is_numeric($v) ? (int) $v : (string) $v; + }, $detach); + } + + // Now we are finally ready to attach the new records. Note that we'll disable + // touching until after the entire operation is complete so we don't fire a + // ton of touch operations until we are totally done syncing the records. + $changes = array_merge( + $changes, + $this->attachNew($records, $current, false), + ); + + if (count($changes['attached']) || count($changes['updated'])) { + $this->touchIfTouching(); + } + + return $changes; + } + + /** @inheritdoc */ + public function updateExistingPivot($id, array $attributes, $touch = true) + { + // Do nothing, we have no pivot table. + } + + /** @inheritdoc */ + public function attach($id, array $attributes = [], $touch = true) + { + if ($id instanceof Model) { + $model = $id; + + $id = $this->parseId($model); + + if ($this->getInverse()) { + // Attach the new ids to the parent model. + $this->parent->push($this->table, [ + [ + $this->relatedPivotKey => $model->{$this->relatedKey}, + $this->morphType => $model->getMorphClass(), + ], + ], true); + + // Attach the new parent id to the related model. + $model->push($this->foreignPivotKey, $this->parseIds($this->parent), true); + } else { + // Attach the new parent id to the related model. + $model->push($this->table, [ + [ + $this->foreignPivotKey => $this->parent->{$this->parentKey}, + $this->morphType => $this->parent instanceof Model ? $this->parent->getMorphClass() : null, + ], + ], true); + + // Attach the new ids to the parent model. + $this->parent->push($this->relatedPivotKey, (array) $id, true); + } + } else { + if ($id instanceof Collection) { + $id = $this->parseIds($id); + } + + $id = (array) $id; + + $query = $this->newRelatedQuery(); + $query->whereIn($this->relatedKey, $id); + + if ($this->getInverse()) { + // Attach the new parent id to the related model. + $query->push($this->foreignPivotKey, $this->parent->{$this->parentKey}); + + // Attach the new ids to the parent model. + foreach ($id as $item) { + $this->parent->push($this->table, [ + [ + $this->relatedPivotKey => $item, + $this->morphType => $this->related instanceof Model ? $this->related->getMorphClass() : null, + ], + ], true); + } + } else { + // Attach the new parent id to the related model. + $query->push($this->table, [ + [ + $this->foreignPivotKey => $this->parent->{$this->parentKey}, + $this->morphType => $this->parent instanceof Model ? $this->parent->getMorphClass() : null, + ], + ], true); + + // Attach the new ids to the parent model. + $this->parent->push($this->relatedPivotKey, $id, true); + } + } + + if ($touch) { + $this->touchIfTouching(); + } + } + + /** @inheritdoc */ + public function detach($ids = [], $touch = true) + { + if ($ids instanceof Model) { + $ids = $this->parseIds($ids); + } + + $query = $this->newRelatedQuery(); + + // If associated IDs were passed to the method we will only delete those + // associations, otherwise all the association ties will be broken. + // 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->getInverse()) { + // Remove the relation from the parent. + $data = []; + foreach ($ids as $item) { + $data = array_merge($data, [ + [ + $this->relatedPivotKey => $item, + $this->morphType => $this->related->getMorphClass(), + ], + ]); + } + + $this->parent->pull($this->table, $data); + + // Prepare the query to select all related objects. + if (count($ids) > 0) { + $query->whereIn($this->relatedKey, $ids); + } + + // Remove the relation from the related. + $query->pull($this->foreignPivotKey, $this->parent->{$this->parentKey}); + } else { + // Remove the relation from the parent. + $this->parent->pull($this->relatedPivotKey, $ids); + + // Prepare the query to select all related objects. + if (count($ids) > 0) { + $query->whereIn($this->relatedKey, $ids); + } + + // Remove the relation to the related. + $query->pull($this->table, [ + [ + $this->foreignPivotKey => $this->parent->{$this->parentKey}, + $this->morphType => $this->parent->getMorphClass(), + ], + ]); + } + + if ($touch) { + $this->touchIfTouching(); + } + + return count($ids); + } + + /** @inheritdoc */ + protected function buildDictionary(Collection $results) + { + $foreign = $this->foreignPivotKey; + + // First we will build a dictionary of child models keyed by the foreign key + // of the relation so that we will easily and quickly match them to their + // parents without having a possibly slow inner loops for every models. + $dictionary = []; + + foreach ($results as $result) { + if ($this->getInverse()) { + foreach ($result->$foreign as $item) { + $dictionary[$item][] = $result; + } + } else { + // Collect $foreign value from pivot column of result model + $items = $this->extractIds($result->{$this->table} ?? [], $foreign); + foreach ($items as $item) { + $dictionary[$item][] = $result; + } + } + } + + return $dictionary; + } + + /** @inheritdoc */ + public function newPivotQuery() + { + return $this->newRelatedQuery(); + } + + /** + * Create a new query builder for the related model. + * + * @return \Illuminate\Database\Query\Builder + */ + public function newRelatedQuery() + { + return $this->related->newQuery(); + } + + /** @inheritdoc */ + public function getQualifiedRelatedPivotKeyName() + { + return $this->relatedPivotKey; + } + + /** + * Get the name of the "where in" method for eager loading. + * + * @param string $key + * + * @return string + */ + protected function whereInMethod(Model $model, $key) + { + return 'whereIn'; + } + + /** + * Extract ids from given pivot table data + * + * @param array $data + * @param string|null $relatedPivotKey + * + * @return mixed + */ + public function extractIds(array $data, ?string $relatedPivotKey = null) + { + $relatedPivotKey = $relatedPivotKey ?: $this->relatedPivotKey; + return array_reduce($data, function ($carry, $item) use ($relatedPivotKey) { + if (is_array($item) && array_key_exists($relatedPivotKey, $item)) { + $carry[] = $item[$relatedPivotKey]; + } + + return $carry; + }, []); + } +} diff --git a/tests/Models/Client.php b/tests/Models/Client.php index 7ee8cec4a..2ab4f5e33 100644 --- a/tests/Models/Client.php +++ b/tests/Models/Client.php @@ -29,4 +29,22 @@ public function addresses(): HasMany { return $this->hasMany(Address::class, 'data.client_id', 'data.client_id'); } + + public function labels() + { + return $this->morphToMany(Label::class, 'labelled'); + } + + public function labelsWithCustomKeys() + { + return $this->morphToMany( + Label::class, + 'clabelled', + 'clabelleds', + 'cclabelled_id', + 'clabel_ids', + 'cclient_id', + 'clabel_id', + ); + } } diff --git a/tests/Models/Label.php b/tests/Models/Label.php new file mode 100644 index 000000000..179503ce1 --- /dev/null +++ b/tests/Models/Label.php @@ -0,0 +1,51 @@ +morphedByMany(User::class, 'labelled'); + } + + public function clients() + { + return $this->morphedByMany(Client::class, 'labelled'); + } + + public function clientsWithCustomKeys() + { + return $this->morphedByMany( + Client::class, + 'clabelled', + 'clabelleds', + 'clabel_ids', + 'cclabelled_id', + 'clabel_id', + 'cclient_id', + ); + } +} diff --git a/tests/Models/User.php b/tests/Models/User.php index 4e0d7294c..f2d2cf7cc 100644 --- a/tests/Models/User.php +++ b/tests/Models/User.php @@ -38,12 +38,22 @@ class User extends Eloquent implements AuthenticatableContract, CanResetPassword use Notifiable; use MassPrunable; - protected $connection = 'mongodb'; - protected $casts = [ + protected $connection = 'mongodb'; + protected $casts = [ 'birthday' => 'datetime', 'entry.date' => 'datetime', 'member_status' => MemberStatus::class, ]; + + protected $fillable = [ + 'name', + 'email', + 'title', + 'age', + 'birthday', + 'username', + 'member_status', + ]; protected static $unguarded = true; public function books() @@ -96,6 +106,11 @@ public function photos() return $this->morphMany(Photo::class, 'has_image'); } + public function labels() + { + return $this->morphToMany(Label::class, 'labelled'); + } + public function addresses() { return $this->embedsMany(Address::class); diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index a4a1c7a84..652f3d7bf 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -13,6 +13,7 @@ use MongoDB\Laravel\Tests\Models\Experience; use MongoDB\Laravel\Tests\Models\Group; use MongoDB\Laravel\Tests\Models\Item; +use MongoDB\Laravel\Tests\Models\Label; use MongoDB\Laravel\Tests\Models\Photo; use MongoDB\Laravel\Tests\Models\Role; use MongoDB\Laravel\Tests\Models\Skill; @@ -33,6 +34,7 @@ public function tearDown(): void Role::truncate(); Group::truncate(); Photo::truncate(); + Label::truncate(); Skill::truncate(); Experience::truncate(); } @@ -491,6 +493,450 @@ public function testMorph(): void $this->assertEquals($client->_id, $photo->hasImageWithCustomOwnerKey->_id); } + public function testMorphToMany(): void + { + $user = User::query()->create(['name' => 'Young Gerald']); + $client = Client::query()->create(['name' => 'Hans Thomas']); + + $label = Label::query()->create(['name' => 'Had the world in my palms, I gave it to you']); + + $user->labels()->attach($label); + $client->labels()->attach($label); + + $this->assertEquals(1, $user->labels->count()); + $this->assertContains($label->_id, $user->labels->pluck('_id')); + + $this->assertEquals(1, $client->labels->count()); + $this->assertContains($label->_id, $user->labels->pluck('_id')); + } + + public function testMorphToManyAttachEloquentCollection(): void + { + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label1 = Label::query()->create(['name' => "Make no mistake, it's the life that I was chosen for"]); + $label2 = Label::query()->create(['name' => 'All I prayed for was an open door']); + + $client->labels()->attach(new Collection([$label1, $label2])); + + $this->assertEquals(2, $client->labels->count()); + $this->assertContains($label1->_id, $client->labels->pluck('_id')); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManyAttachMultipleIds(): void + { + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label1 = Label::query()->create(['name' => 'stayed solid i never fled']); + $label2 = Label::query()->create(['name' => "I've got a lane and I'm in gear"]); + + $client->labels()->attach([$label1->_id, $label2->_id]); + + $this->assertEquals(2, $client->labels->count()); + $this->assertContains($label1->_id, $client->labels->pluck('_id')); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManyDetaching(): void + { + $client = Client::query()->create(['name' => 'Marshall Mathers']); + + $label1 = Label::query()->create(['name' => "I'll never love again"]); + $label2 = Label::query()->create(['name' => 'The way I loved you']); + + $client->labels()->attach([$label1->_id, $label2->_id]); + + $this->assertEquals(2, $client->labels->count()); + + $client->labels()->detach($label1); + $check = $client->withoutRelations(); + + $this->assertEquals(1, $check->labels->count()); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManyDetachingMultipleIds(): void + { + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label1 = Label::query()->create(['name' => "I make what I wanna make, but I won't make everyone happy"]); + $label2 = Label::query()->create(['name' => "My skin's thick, but I'm not bulletproof"]); + $label3 = Label::query()->create(['name' => 'All I can be is myself, go, and tell the truth']); + + $client->labels()->attach([$label1->_id, $label2->_id, $label3->_id]); + + $this->assertEquals(3, $client->labels->count()); + + $client->labels()->detach([$label1->_id, $label2->_id]); + $client->refresh(); + + $this->assertEquals(1, $client->labels->count()); + $this->assertContains($label3->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManySyncing(): void + { + $user = User::query()->create(['name' => 'Young Gerald']); + $client = Client::query()->create(['name' => 'Hans Thomas']); + + $label = Label::query()->create(['name' => "Lesson learned, we weren't the perfect match"]); + $label2 = Label::query()->create(['name' => 'Future ref, not keeping personal and work attached']); + + $user->labels()->sync($label); + $client->labels()->sync($label); + $client->labels()->sync($label2, false); + + $this->assertEquals(1, $user->labels->count()); + $this->assertContains($label->_id, $user->labels->pluck('_id')); + $this->assertNotContains($label2->_id, $user->labels->pluck('_id')); + + $this->assertEquals(2, $client->labels->count()); + $this->assertContains($label->_id, $client->labels->pluck('_id')); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManySyncingEloquentCollection(): void + { + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label = Label::query()->create(['name' => 'Why the ones who love me most, the people I push away?']); + $label2 = Label::query()->create(['name' => 'Look in a mirror, this is you']); + + $client->labels()->sync(new Collection([$label, $label2])); + + $this->assertEquals(2, $client->labels->count()); + $this->assertContains($label->_id, $client->labels->pluck('_id')); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManySyncingMultipleIds(): void + { + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label = Label::query()->create(['name' => 'They all talk about karma, how it slowly comes']); + $label2 = Label::query()->create(['name' => "But life is short, enjoy it while you're young"]); + + $client->labels()->sync([$label->_id, $label2->_id]); + + $this->assertEquals(2, $client->labels->count()); + $this->assertContains($label->_id, $client->labels->pluck('_id')); + $this->assertContains($label2->_id, $client->labels->pluck('_id')); + } + + public function testMorphToManySyncingWithCustomKeys(): void + { + $client = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Young Gerald']); + + $label = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "Why do people do things that be bad for 'em?"]); + $label2 = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "Say we done with these things, then we ask for 'em"]); + + $client->labelsWithCustomKeys()->sync([$label->clabel_id, $label2->clabel_id]); + + $this->assertEquals(2, $client->labelsWithCustomKeys->count()); + $this->assertContains($label->_id, $client->labelsWithCustomKeys->pluck('_id')); + $this->assertContains($label2->_id, $client->labelsWithCustomKeys->pluck('_id')); + + $client->labelsWithCustomKeys()->sync($label); + $client->load('labelsWithCustomKeys'); + + $this->assertEquals(1, $client->labelsWithCustomKeys->count()); + $this->assertContains($label->_id, $client->labelsWithCustomKeys->pluck('_id')); + $this->assertNotContains($label2->_id, $client->labelsWithCustomKeys->pluck('_id')); + } + + public function testMorphToManyLoadAndRefreshing(): void + { + $user = User::query()->create(['name' => 'The Pretty Reckless']); + + $client = Client::query()->create(['name' => 'Young Gerald']); + + $label = Label::query()->create(['name' => 'The greatest gift is knowledge itself']); + $label2 = Label::query()->create(['name' => "I made it here all by my lonely, no askin' for help"]); + + $client->labels()->sync([$label->_id, $label2->_id]); + $client->users()->sync($user); + + $this->assertEquals(2, $client->labels->count()); + + $client->load('labels'); + + $this->assertEquals(2, $client->labels->count()); + + $client->refresh(); + + $this->assertEquals(2, $client->labels->count()); + + $check = Client::query()->find($client->_id); + + $this->assertEquals(2, $check->labels->count()); + + $check = Client::query()->with('labels')->find($client->_id); + + $this->assertEquals(2, $check->labels->count()); + } + + public function testMorphToManyHasQuery(): void + { + $client = Client::query()->create(['name' => 'Ashley']); + $client2 = Client::query()->create(['name' => 'Halsey']); + $client3 = Client::query()->create(['name' => 'John Doe 2']); + + $label = Label::query()->create(['name' => "I've been digging myself down deeper"]); + $label2 = Label::query()->create(['name' => "I won't stop 'til I get where you are"]); + + $client->labels()->sync([$label->_id, $label2->_id]); + $client2->labels()->sync($label); + + $this->assertEquals(2, $client->labels->count()); + $this->assertEquals(1, $client2->labels->count()); + + $check = Client::query()->has('labels')->get(); + $this->assertCount(2, $check); + + $check = Client::query()->has('labels', '>', 1)->get(); + $this->assertCount(1, $check); + $this->assertContains($client->_id, $check->pluck('_id')); + + $check = Client::query()->has('labels', '<', 2)->get(); + $this->assertCount(2, $check); + $this->assertContains($client2->_id, $check->pluck('_id')); + $this->assertContains($client3->_id, $check->pluck('_id')); + } + + public function testMorphedByMany(): void + { + $user = User::query()->create(['name' => 'Young Gerald']); + $client = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => 'Never finished, tryna search for more']); + + $label->users()->attach($user); + $label->clients()->attach($client); + + $this->assertEquals(1, $label->users->count()); + $this->assertContains($user->_id, $label->users->pluck('_id')); + + $this->assertEquals(1, $label->clients->count()); + $this->assertContains($client->_id, $label->clients->pluck('_id')); + } + + public function testMorphedByManyAttachEloquentCollection(): void + { + $client1 = Client::query()->create(['name' => 'Young Gerald']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => 'They want me to architect Rome, in a day']); + + $label->clients()->attach(new Collection([$client1, $client2])); + + $this->assertEquals(2, $label->clients->count()); + $this->assertContains($client1->_id, $label->clients->pluck('_id')); + $this->assertContains($client2->_id, $label->clients->pluck('_id')); + + $client1->refresh(); + $this->assertEquals(1, $client1->labels->count()); + } + + public function testMorphedByManyAttachMultipleIds(): void + { + $client1 = Client::query()->create(['name' => 'Austin Richard Post']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => 'Always in the game and never played by the rules']); + + $label->clients()->attach([$client1->_id, $client2->_id]); + + $this->assertEquals(2, $label->clients->count()); + $this->assertContains($client1->_id, $label->clients->pluck('_id')); + $this->assertContains($client2->_id, $label->clients->pluck('_id')); + + $client1->refresh(); + $this->assertEquals(1, $client1->labels->count()); + } + + public function testMorphedByManyDetaching(): void + { + $client1 = Client::query()->create(['name' => 'Austin Richard Post']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => 'Seasons change and our love went cold']); + + $label->clients()->attach([$client1->_id, $client2->_id]); + + $this->assertEquals(2, $label->clients->count()); + + $label->clients()->detach($client1->_id); + $check = $label->withoutRelations(); + + $this->assertEquals(1, $check->clients->count()); + $this->assertContains($client2->_id, $check->clients->pluck('_id')); + } + + public function testMorphedByManyDetachingMultipleIds(): void + { + $client1 = Client::query()->create(['name' => 'Austin Richard Post']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $client3 = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "Run away, but we're running in circles"]); + + $label->clients()->attach([$client1->_id, $client2->_id, $client3->_id]); + + $this->assertEquals(3, $label->clients->count()); + + $label->clients()->detach([$client1->_id, $client2->_id]); + $label->load('clients'); + + $this->assertEquals(1, $label->clients->count()); + $this->assertContains($client3->_id, $label->clients->pluck('_id')); + } + + public function testMorphedByManySyncing(): void + { + $client1 = Client::query()->create(['name' => 'Austin Richard Post']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $client3 = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "Was scared of losin' somethin' that we never found"]); + + $label->clients()->sync($client1); + $label->clients()->sync($client2, false); + $label->clients()->sync($client3, false); + + $this->assertEquals(3, $label->clients->count()); + $this->assertContains($client1->_id, $label->clients->pluck('_id')); + $this->assertContains($client2->_id, $label->clients->pluck('_id')); + $this->assertContains($client3->_id, $label->clients->pluck('_id')); + } + + public function testMorphedByManySyncingEloquentCollection(): void + { + $client1 = Client::query()->create(['name' => 'Austin Richard Post']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "I'm goin' hard 'til I'm gone. Can you feel it?"]); + + $label->clients()->sync(new Collection([$client1, $client2])); + + $this->assertEquals(2, $label->clients->count()); + $this->assertContains($client1->_id, $label->clients->pluck('_id')); + $this->assertContains($client2->_id, $label->clients->pluck('_id')); + + $this->assertNotContains($extra->_id, $label->clients->pluck('_id')); + } + + public function testMorphedByManySyncingMultipleIds(): void + { + $client1 = Client::query()->create(['name' => 'Dorothy']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $extra = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "Love ain't patient, it's not kind. true love waits to rob you blind"]); + + $label->clients()->sync([$client1->_id, $client2->_id]); + + $this->assertEquals(2, $label->clients->count()); + $this->assertContains($client1->_id, $label->clients->pluck('_id')); + $this->assertContains($client2->_id, $label->clients->pluck('_id')); + + $this->assertNotContains($extra->_id, $label->clients->pluck('_id')); + } + + public function testMorphedByManySyncingWithCustomKeys(): void + { + $client1 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Young Gerald']); + $client2 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'Hans Thomas']); + $client3 = Client::query()->create(['cclient_id' => (string) (new ObjectId()), 'name' => 'John Doe']); + + $label = Label::query()->create(['clabel_id' => (string) (new ObjectId()), 'name' => "I'm in my own lane, so what do I have to hurry for?"]); + + $label->clientsWithCustomKeys()->sync([$client1->cclient_id, $client2->cclient_id]); + + $this->assertEquals(2, $label->clientsWithCustomKeys->count()); + $this->assertContains($client1->_id, $label->clientsWithCustomKeys->pluck('_id')); + $this->assertContains($client2->_id, $label->clientsWithCustomKeys->pluck('_id')); + + $this->assertNotContains($client3->_id, $label->clientsWithCustomKeys->pluck('_id')); + + $label->clientsWithCustomKeys()->sync($client3); + $label->load('clientsWithCustomKeys'); + + $this->assertEquals(1, $label->clientsWithCustomKeys->count()); + $this->assertNotContains($client1->_id, $label->clientsWithCustomKeys->pluck('_id')); + $this->assertNotContains($client2->_id, $label->clientsWithCustomKeys->pluck('_id')); + + $this->assertContains($client3->_id, $label->clientsWithCustomKeys->pluck('_id')); + } + + public function testMorphedByManyLoadAndRefreshing(): void + { + $user = User::query()->create(['name' => 'Abel Tesfaye']); + + $client1 = Client::query()->create(['name' => 'Young Gerald']); + $client2 = Client::query()->create(['name' => 'Hans Thomas']); + $client3 = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "but don't think I don't think about you just cause I ain't spoken about you"]); + + $label->clients()->sync(new Collection([$client1, $client2, $client3])); + $label->users()->sync($user); + + $this->assertEquals(3, $label->clients->count()); + + $label->load('clients'); + + $this->assertEquals(3, $label->clients->count()); + + $label->refresh(); + + $this->assertEquals(3, $label->clients->count()); + + $check = Label::query()->find($label->_id); + + $this->assertEquals(3, $check->clients->count()); + + $check = Label::query()->with('clients')->find($label->_id); + + $this->assertEquals(3, $check->clients->count()); + } + + public function testMorphedByManyHasQuery(): void + { + $user = User::query()->create(['name' => 'Austin Richard Post']); + + $client1 = Client::query()->create(['name' => 'Young Gerald']); + $client2 = Client::query()->create(['name' => 'John Doe']); + + $label = Label::query()->create(['name' => "My star's back shining bright, I just polished it"]); + $label2 = Label::query()->create(['name' => "Somethin' in my spirit woke back up like I just sat up"]); + $label3 = Label::query()->create(['name' => 'How can I beam when you blocking my light?']); + + $label->clients()->sync(new Collection([$client1, $client2])); + $label2->clients()->sync($client1); + $label3->users()->sync($user); + + $this->assertEquals(2, $label->clients->count()); + + $check = Label::query()->has('clients')->get(); + $this->assertCount(2, $check); + $this->assertContains($label->_id, $check->pluck('_id')); + $this->assertContains($label2->_id, $check->pluck('_id')); + + $check = Label::query()->has('users')->get(); + $this->assertCount(1, $check); + $this->assertContains($label3->_id, $check->pluck('_id')); + + $check = Label::query()->has('clients', '>', 1)->get(); + $this->assertCount(1, $check); + $this->assertContains($label->_id, $check->pluck('_id')); + } + public function testHasManyHas(): void { $author1 = User::create(['name' => 'George R. R. Martin']);