diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 9ce32d4a..763ed034 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -169,7 +169,22 @@ public function getRelationDefinition($name) public function getRelationTypeDefinitions($type) { if (in_array($type, static::$relationTypes)) { - return $this->{$type}; + $definitions = $this->{$type} ?: []; + + // Handle renaming otherKey to relatedKey + // @see https://github.com/laravel/framework/commit/d2a77776295cb155b985526c1fa1fddc190adb07 + // @since v1.1.8 + foreach ($definitions as $relation => $options) { + if ( + is_array($options) + && array_key_exists('otherKey', $options) + && !array_key_exists('relatedPivotKey', $options) + ) { + $definitions[$relation]['relatedPivotKey'] = $options['otherKey']; + } + } + + return $definitions; } return []; @@ -309,18 +324,21 @@ protected function handleRelation($relationName) switch ($relationType) { case 'hasOne': case 'hasMany': - $relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']); - $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName); + $relation = $this->validateRelationArgs($relationName, ['key', 'relatedKey']); + $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['relatedKey'], $relationName); break; case 'belongsTo': - $relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']); - $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName); + $relation = $this->validateRelationArgs($relationName, ['key', 'relatedKey']); + $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['relatedKey'], $relationName); break; case 'belongsToMany': - $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps']); - $relationObj = $this->$relationType($relation[0], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName); + $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'relatedPivotKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps']); + $relationObj = $this->$relationType($relation[0], $relation['table'], $relation['key'], $relation['relatedPivotKey'], $relation['parentKey'], $relation['relatedKey'], $relationName); + if (!empty($relation['pivotModel'])) { + $relationObj->using($relation['pivotModel']); + } break; case 'morphTo': @@ -335,13 +353,19 @@ protected function handleRelation($relationName) break; case 'morphToMany': - $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']); - $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], false, $relationName); + $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'relatedPivotKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']); + $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['relatedPivotKey'], $relation['parentKey'], $relation['relatedKey'], false, $relationName); + if (!empty($relation['pivotModel'])) { + $relationObj->using($relation['pivotModel']); + } break; case 'morphedByMany': - $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']); - $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName); + $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'relatedPivotKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']); + $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['relatedPivotKey'], $relation['parentKey'], $relation['relatedKey'], $relationName); + if (!empty($relation['pivotModel'])) { + $relationObj->using($relation['pivotModel']); + } break; case 'attachOne': @@ -352,8 +376,8 @@ protected function handleRelation($relationName) case 'hasOneThrough': case 'hasManyThrough': - $relation = $this->validateRelationArgs($relationName, ['key', 'throughKey', 'otherKey', 'secondOtherKey'], ['through']); - $relationObj = $this->$relationType($relation[0], $relation['through'], $relation['key'], $relation['throughKey'], $relation['otherKey'], $relation['secondOtherKey']); + $relation = $this->validateRelationArgs($relationName, ['key', 'throughKey', 'relatedKey', 'secondOtherKey'], ['through']); + $relationObj = $this->$relationType($relation[0], $relation['through'], $relation['key'], $relation['throughKey'], $relation['relatedKey'], $relation['secondOtherKey']); break; default: diff --git a/src/Database/Model.php b/src/Database/Model.php index 2add8b61..1fe7e12a 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -723,7 +723,7 @@ public function newPivot(EloquentModel $parent, array $attributes, $table, $exis { return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists) - : new Pivot($parent, $attributes, $table, $exists); + : Pivot::fromAttributes($parent, $attributes, $table, $exists); } /** @@ -741,7 +741,7 @@ public function newRelationPivot($relationName, $parent, $attributes, $table, $e if (!is_null($definition) && array_key_exists('pivotModel', $definition)) { $pivotModel = $definition['pivotModel']; - return new $pivotModel($parent, $attributes, $table, $exists); + return $pivotModel::fromAttributes($parent, $attributes, $table, $exists); } } diff --git a/src/Database/Pivot.php b/src/Database/Pivot.php index 5172b7df..203d6e46 100644 --- a/src/Database/Pivot.php +++ b/src/Database/Pivot.php @@ -1,7 +1,8 @@ timestamps = $instance->hasTimestampAttributes($attributes); // The pivot model is a "dynamic" model since we will set the tables dynamically // for the instance. This allows it work for any intermediate tables for the // many to many relationship that are defined by this developer's classes. - $this->setRawAttributes($attributes, true); - - $this->setTable($table); - - $this->setConnection($parent->getConnectionName()); + $instance->setConnection($parent->getConnectionName()) + ->setTable($table) + ->forceFill($attributes) + ->syncOriginal(); // We store off the parent instance so we will access the timestamp column names // for the model, since the pivot model timestamps aren't easily configurable // from the developer's point of view. We can use the parents to get these. - $this->parent = $parent; + $instance->pivotParent = $parent; + + $instance->exists = $exists; + + return $instance; + } + + /** + * Create a new pivot model from raw values returned from a query. + * + * @param \Illuminate\Database\Eloquent\Model $parent + * @param array $attributes + * @param string $table + * @param bool $exists + * @return static + */ + public static function fromRawAttributes(ModelBase $parent, $attributes, $table, $exists = false) + { + $instance = static::fromAttributes($parent, [], $table, $exists); + + $instance->timestamps = $instance->hasTimestampAttributes($attributes); - $this->exists = $exists; + $instance->setRawAttributes($attributes, $exists); - $this->timestamps = $this->hasTimestampAttributes(); + return $instance; } /** * Set the keys for a save update query. * - * @param \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery(BuilderBase $query) + protected function setKeysForSaveQuery(Builder $query) { - $query->where($this->foreignKey, $this->getAttribute($this->foreignKey)); + if (isset($this->attributes[$this->getKeyName()])) { + return parent::setKeysForSaveQuery($query); + } - return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); + $query->where($this->foreignKey, $this->getOriginal( + $this->foreignKey, + $this->getAttribute($this->foreignKey) + )); + + return $query->where($this->relatedKey, $this->getOriginal( + $this->relatedKey, + $this->getAttribute($this->relatedKey) + )); } /** @@ -85,7 +117,19 @@ protected function setKeysForSaveQuery(BuilderBase $query) */ public function delete() { - return $this->getDeleteQuery()->delete(); + if (isset($this->attributes[$this->getKeyName()])) { + return (int) parent::delete(); + } + + if ($this->fireModelEvent('deleting') === false) { + return 0; + } + + $this->touchOwners(); + + return tap($this->getDeleteQuery()->delete(), function () { + $this->fireModelEvent('deleted', false); + }); } /** @@ -95,11 +139,28 @@ public function delete() */ protected function getDeleteQuery() { - $foreign = $this->getAttribute($this->foreignKey); + return $this->newQueryWithoutRelationships()->where([ + $this->foreignKey => $this->getOriginal($this->foreignKey, $this->getAttribute($this->foreignKey)), + $this->relatedKey => $this->getOriginal($this->relatedKey, $this->getAttribute($this->relatedKey)), + ]); + } - $query = $this->newQuery()->where($this->foreignKey, $foreign); + /** + * Get the table associated with the model. + * + * @return string + */ + public function getTable() + { + if (!isset($this->table)) { + $this->setTable(str_replace( + '\\', + '', + Str::snake(Str::singular(class_basename($this))) + )); + } - return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); + return $this->table; } /** @@ -113,39 +174,50 @@ public function getForeignKey() } /** - * Get the "other key" column name. + * Get the "related key" column name. + * + * @return string + */ + public function getRelatedKey() + { + return $this->relatedKey; + } + + /** + * Get the "related key" column name. * * @return string */ public function getOtherKey() { - return $this->otherKey; + return $this->getRelatedKey(); } /** * Set the key names for the pivot model instance. * * @param string $foreignKey - * @param string $otherKey + * @param string $relatedKey * @return $this */ - public function setPivotKeys($foreignKey, $otherKey) + public function setPivotKeys($foreignKey, $relatedKey) { $this->foreignKey = $foreignKey; - $this->otherKey = $otherKey; + $this->relatedKey = $relatedKey; return $this; } /** - * Determine if the pivot model has timestamp attributes. + * Determine if the pivot model or given attributes has timestamp attributes. * + * @param array|null $attributes * @return bool */ - public function hasTimestampAttributes() + public function hasTimestampAttributes($attributes = null) { - return array_key_exists($this->getCreatedAtColumn(), $this->attributes); + return array_key_exists($this->getCreatedAtColumn(), $attributes ?? $this->attributes); } /** @@ -155,7 +227,9 @@ public function hasTimestampAttributes() */ public function getCreatedAtColumn() { - return $this->parent->getCreatedAtColumn(); + return $this->pivotParent + ? $this->pivotParent->getCreatedAtColumn() + : parent::getCreatedAtColumn(); } /** @@ -165,6 +239,92 @@ public function getCreatedAtColumn() */ public function getUpdatedAtColumn() { - return $this->parent->getUpdatedAtColumn(); + return $this->pivotParent + ? $this->pivotParent->getUpdatedAtColumn() + : parent::getUpdatedAtColumn(); + } + + /** + * Get the queueable identity for the entity. + * + * @return mixed + */ + public function getQueueableId() + { + if (isset($this->attributes[$this->getKeyName()])) { + return $this->getKey(); + } + + return sprintf( + '%s:%s:%s:%s', + $this->foreignKey, + $this->getAttribute($this->foreignKey), + $this->relatedKey, + $this->getAttribute($this->relatedKey) + ); + } + + /** + * Get a new query to restore one or more models by their queueable IDs. + * + * @param int[]|string[]|string $ids + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQueryForRestoration($ids) + { + if (is_array($ids)) { + return $this->newQueryForCollectionRestoration($ids); + } + + if (! Str::contains($ids, ':')) { + return parent::newQueryForRestoration($ids); + } + + $segments = explode(':', $ids); + + return $this->newQueryWithoutScopes() + ->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]); + } + + /** + * Get a new query to restore multiple models by their queueable IDs. + * + * @param int[]|string[] $ids + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function newQueryForCollectionRestoration(array $ids) + { + $ids = array_values($ids); + + if (! Str::contains($ids[0], ':')) { + return parent::newQueryForRestoration($ids); + } + + $query = $this->newQueryWithoutScopes(); + + foreach ($ids as $id) { + $segments = explode(':', $id); + + $query->orWhere(function ($query) use ($segments) { + return $query->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]); + }); + } + + return $query; + } + + /** + * Unset all the loaded relations for the instance. + * + * @return $this + */ + public function unsetRelations() + { + $this->pivotParent = null; + $this->relations = []; + + return $this; } } diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index f498c364..db730e67 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -28,6 +28,8 @@ class BelongsToMany extends BelongsToManyBase * @param string $table * @param string $foreignPivotKey * @param string $relatedPivotKey + * @param string $parentKey + * @param string $relatedKey * @param string $relationName * @return void */ @@ -97,9 +99,9 @@ public function save(Model $model, array $pivotData = [], $sessionKey = null) public function sync($ids, $detaching = true) { $changed = parent::sync($ids, $detaching); - + $this->flushDuplicateCache(); - + return $changed; } @@ -121,6 +123,7 @@ public function create(array $attributes = [], array $pivotData = [], $sessionKe * @param mixed $id * @param array $attributes * @param bool $touch + * @return void */ public function attach($id, array $attributes = [], $touch = true) { @@ -145,14 +148,7 @@ public function attach($id, array $attributes = [], $touch = true) return; } - // Here we will insert the attachment records into the pivot table. Once we have - // inserted the records, we will touch the relationships if necessary and the - // function will return. We can parse the IDs before inserting the records. - $this->newPivotStatement()->insert($insertData); - - if ($touch) { - $this->touchIfTouching(); - } + parent::attach($id, $attributes, $touch); /** * @event model.relation.afterAttach diff --git a/src/Database/Relations/MorphTo.php b/src/Database/Relations/MorphTo.php index 92e567ea..2dfef088 100644 --- a/src/Database/Relations/MorphTo.php +++ b/src/Database/Relations/MorphTo.php @@ -13,11 +13,11 @@ class MorphTo extends MorphToBase */ protected $relationName; - public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relationName) + public function __construct(Builder $query, Model $parent, $foreignKey, $relatedKey, $type, $relationName) { $this->relationName = $relationName; - parent::__construct($query, $parent, $foreignKey, $otherKey, $type, $relationName); + parent::__construct($query, $parent, $foreignKey, $relatedKey, $type, $relationName); $this->addDefinedConstraints(); } diff --git a/src/Database/Relations/MorphToMany.php b/src/Database/Relations/MorphToMany.php index b2771ca0..6a168a29 100644 --- a/src/Database/Relations/MorphToMany.php +++ b/src/Database/Relations/MorphToMany.php @@ -45,8 +45,10 @@ class MorphToMany extends BelongsToMany * @param \Illuminate\Database\Eloquent\Model $parent * @param string $name * @param string $table - * @param string $foreignKey - * @param string $otherKey + * @param string $foreignPivotKey + * @param string $relatedPivotKey + * @param string $parentKey + * @param string $relatedKey * @param string $relationName * @param bool $inverse * @return void @@ -56,8 +58,8 @@ public function __construct( Model $parent, $name, $table, - $foreignKey, - $otherKey, + $foreignPivotKey, + $relatedPivotKey, $parentKey, $relatedKey, $relationName = null, @@ -73,8 +75,8 @@ public function __construct( $query, $parent, $table, - $foreignKey, - $otherKey, + $foreignPivotKey, + $relatedPivotKey, $parentKey, $relatedKey, $relationName diff --git a/tests/Database/RelationsTest.php b/tests/Database/RelationsTest.php index 93a1a854..2bf3c8fa 100644 --- a/tests/Database/RelationsTest.php +++ b/tests/Database/RelationsTest.php @@ -304,28 +304,30 @@ class Post extends \Winter\Storm\Database\Model ], 'tags' => [ Term::class, - 'table' => 'posts_terms', - 'key' => 'post_id', - 'otherKey' => 'term_id', - 'pivot' => ['data'], + 'table' => 'posts_terms', + 'key' => 'post_id', + // Explicitly test pre: 1.1.8 option name + // @see https://github.com/laravel/framework/commit/d2a77776295cb155b985526c1fa1fddc190adb07 + 'otherKey' => 'term_id', + 'pivot' => ['data'], 'timestamps' => true, 'conditions' => 'type = "tag"', ], 'labels' => [ Term::class, - 'table' => 'posts_terms', - 'key' => 'post_id', - 'otherKey' => 'term_id', - 'pivot' => ['data'], - 'timestamps' => true, - 'conditions' => 'type = "label"', + 'table' => 'posts_terms', + 'key' => 'post_id', + 'relatedPivotKey' => 'term_id', + 'pivot' => ['data'], + 'timestamps' => true, + 'conditions' => 'type = "label"', ], 'terms' => [ Term::class, - 'table' => 'posts_terms', - 'key' => 'post_id', - 'otherKey' => 'term_id', - 'timestamps' => true, + 'table' => 'posts_terms', + 'key' => 'post_id', + 'relatedPivotKey' => 'term_id', + 'timestamps' => true, ], ]; @@ -357,8 +359,8 @@ class Term extends \Winter\Storm\Database\Model 'Post', 'table' => 'posts_terms', 'key' => 'term_id', - 'otherKey' => 'post_id', - 'pivot' => ['data'], + 'relatedKey' => 'post_id', + 'pivot' => ['data'], 'timestamps' => true, 'conditions' => 'type = "post"', ],