diff --git a/src/Eloquent/HybridRelations.php b/src/Eloquent/HybridRelations.php index 9d6aa90e1..ddb102a3f 100644 --- a/src/Eloquent/HybridRelations.php +++ b/src/Eloquent/HybridRelations.php @@ -15,6 +15,8 @@ use MongoDB\Laravel\Relations\HasOne; use MongoDB\Laravel\Relations\MorphMany; use MongoDB\Laravel\Relations\MorphTo; +use MongoDB\Laravel\Relations\MorphToMany; + use function debug_backtrace; use function is_subclass_of; @@ -42,7 +44,7 @@ trait HybridRelations public function hasOne($related, $foreignKey = null, $localKey = null) { // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::hasOne($related, $foreignKey, $localKey); } @@ -71,7 +73,7 @@ public function hasOne($related, $foreignKey = null, $localKey = null) public function morphOne($related, $name, $type = null, $id = null, $localKey = null) { // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::morphOne($related, $name, $type, $id, $localKey); } @@ -98,7 +100,7 @@ public function morphOne($related, $name, $type = null, $id = null, $localKey = public function hasMany($related, $foreignKey = null, $localKey = null) { // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::hasMany($related, $foreignKey, $localKey); } @@ -127,7 +129,7 @@ public function hasMany($related, $foreignKey = null, $localKey = null) public function morphMany($related, $name, $type = null, $id = null, $localKey = null) { // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::morphMany($related, $name, $type, $id, $localKey); } @@ -167,7 +169,7 @@ public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relat } // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::belongsTo($related, $foreignKey, $ownerKey, $relation); } @@ -279,7 +281,7 @@ public function belongsToMany( } // Check if it is a relation with an original model. - if (! is_subclass_of($related, MongoDBModel::class)) { + if (!is_subclass_of($related, MongoDBModel::class)) { return parent::belongsToMany( $related, $collection, @@ -324,6 +326,124 @@ public function belongsToMany( ); } + /** + * Define a many-to-many relationship. + * + * @param string $related + * @param string $collection + * @param string $foreignKey + * @param string $otherKey + * @param string $parentKey + * @param string $relatedKey + * @param string $relation + * @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 + ) { + + // Check if it is a relation with an original model. + if (!is_subclass_of($related, \Mongodb\Laravel\Eloquent\Model::class)) { + return parent::MorphToMany( + $related, + $name, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey, + $relatedKey, + $inverse, + ); + } + + $caller = $this->guessBelongsToManyRelation(); + + $instance = new $related; + + $foreignPivotKey = $foreignPivotKey ?: $name . '_id'; + + $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey() . 's'; + + // 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 hydrations. + 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(), + $caller, + $inverse + ); + } + + /** + * Define a polymorphic, inverse many-to-many relationship. + * + * @param string $related + * @param string $name + * @param string|null $table + * @param string|null $foreignPivotKey + * @param string|null $relatedPivotKey + * @param string|null $parentKey + * @param string|null $relatedKey + * @param string|null $relation + * @param bool $inverse + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany + */ + public function morphedByMany( + $related, + $name, + $table = null, + $foreignPivotKey = null, + $relatedPivotKey = null, + $parentKey = null, + $relatedKey = null, + $inverse = false + ) { + $caller = $this->guessBelongsToManyRelation(); + + // $instance = new $related; + // 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'; + + $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey() . 's'; + + return $this->morphToMany( + $related, + $name, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey, + $relatedKey, + true + ); + } + + /** * Get the relationship name of the belongs to many. * diff --git a/src/Relations/MorphToMany.php b/src/Relations/MorphToMany.php new file mode 100644 index 000000000..0305117ad --- /dev/null +++ b/src/Relations/MorphToMany.php @@ -0,0 +1,227 @@ +inverse = $inverse; + $this->morphType = $name . '_type'; + $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass(); + + parent::__construct( + $query, + $parent, + $table, + $foreignPivotKey, + $relatedPivotKey, + $parentKey, + $relatedKey, + $relationName + ); + } + + /** + * Set the where clause for the relation query. + * + * @return $this + */ + protected function addWhereConstraints() + { + parent::addWhereConstraints(); + + $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + + return $this; + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + parent::addEagerConstraints($models); + + $this->query->where($this->qualifyPivotColumn($this->morphType), $this->morphClass); + } + + /** + * Create a new pivot attachment record. + * + * @param int $id + * @param bool $timed + * @return array + */ + protected function baseAttachRecord($id, $timed) + { + return Arr::add( + parent::baseAttachRecord($id, $timed), + $this->morphType, + $this->morphClass + ); + } + + /** + * Add the constraints for a relationship count query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + { + return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( + $this->qualifyPivotColumn($this->morphType), + $this->morphClass + ); + } + + /** + * Get the pivot models that are currently attached. + * + * @return \Illuminate\Support\Collection + */ + protected function getCurrentlyAttachedPivots() + { + return parent::getCurrentlyAttachedPivots()->map(function ($record) { + return $record instanceof MorphPivot + ? $record->setMorphType($this->morphType) + ->setMorphClass($this->morphClass) + : $record; + }); + } + + /** + * Create a new query builder for the pivot table. + * + * @return \Illuminate\Database\Query\Builder + */ + public function newPivotQuery() + { + return parent::newPivotQuery()->where($this->morphType, $this->morphClass); + } + + /** + * Create a new pivot model instance. + * + * @param array $attributes + * @param bool $exists + * @return \Illuminate\Database\Eloquent\Relations\Pivot + */ + public function newPivot(array $attributes = [], $exists = false) + { + $using = $this->using; + + $pivot = $using ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists) + : MorphPivot::fromAttributes($this->parent, $attributes, $this->table, $exists); + + $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey) + ->setMorphType($this->morphType) + ->setMorphClass($this->morphClass); + + return $pivot; + } + + /** + * Get the pivot columns for the relation. + * + * "pivot_" is prefixed at each column for easy removal later. + * + * @return array + */ + protected function aliasedPivotColumns() + { + $defaults = [$this->foreignPivotKey, $this->relatedPivotKey, $this->morphType]; + + return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { + return $this->qualifyPivotColumn($column) . ' as pivot_' . $column; + })->unique()->all(); + } + + /** + * Get the foreign key "type" name. + * + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + + /** + * Get the class name of the parent model. + * + * @return string + */ + public function getMorphClass() + { + return $this->morphClass; + } + + /** + * Get the indicator for a reverse relationship. + * + * @return bool + */ + public function getInverse() + { + return $this->inverse; + } +}