diff --git a/src/Database/Concerns/HasReplication.php b/src/Database/Concerns/HasReplication.php index 05eb73f67..b9c6422c2 100644 --- a/src/Database/Concerns/HasReplication.php +++ b/src/Database/Concerns/HasReplication.php @@ -19,12 +19,42 @@ trait HasReplication */ public function replicateWithRelations(array $except = null) { + return $this->replicateRelationsInternal($except); + } + + /** + * duplicateWithRelations replicates a model with special multisite duplication logic. + * To avoid duplication of has many relations, the logic only propagates relations on + * the parent model since they are shared via site_root_id beyond this point. + * + * @param array|null $except + * @return static + */ + public function duplicateWithRelations(array $except = null) + { + return $this->replicateRelationsInternal($except, ['isDuplicate' => true]); + } + + /** + * replicateRelationsInternal + */ + protected function replicateRelationsInternal(array $except = null, array $options = []) + { + extract(array_merge([ + 'isDuplicate' => false + ], $options)); + $defaults = [ $this->getKeyName(), $this->getCreatedAtColumn(), $this->getUpdatedAtColumn(), ]; + $isMultisite = $this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class); + if ($isMultisite) { + $defaults[] = 'site_root_id'; + } + $attributes = Arr::except( $this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults ); @@ -39,7 +69,7 @@ public function replicateWithRelations(array $except = null) foreach ($definitions as $type => $relations) { foreach ($relations as $name => $options) { - if ($this->isRelationReplicable($name)) { + if ($this->isRelationReplicable($name, $isMultisite, $isDuplicate)) { $this->replicateRelationInternal($instance->$name(), $this->$name); } } @@ -77,13 +107,18 @@ protected function replicateRelationInternal($relationObject, $models) * isRelationReplicable determines whether the specified relation should be replicated * when replicateWithRelations() is called instead of save() on the model. Default: true. */ - protected function isRelationReplicable(string $name): bool + protected function isRelationReplicable(string $name, bool $isMultisite, bool $isDuplicate): bool { $relationType = $this->getRelationType($name); if ($relationType === 'morphTo') { return false; } + // Relation is shared via propagation + if (!$isDuplicate && $isMultisite && $this->isAttributePropagatable($name)) { + return false; + } + $definition = $this->getRelationDefinition($name); if (!array_key_exists('replicate', $definition)) { return true; diff --git a/src/Database/Model.php b/src/Database/Model.php index 4ac79ef1d..9ad899b18 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -421,6 +421,9 @@ protected function saveInternal($options = []) $this->commitDeferredAfter($this->sessionKey); } + // After save deferred binding + $this->fireEvent('model.saveComplete'); + return $result; } diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index 29e06e676..f20c3a336 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -110,9 +110,8 @@ public function addEagerConstraints(array $models) */ public function save(Model $model, $sessionKey = null) { - // Delete siblings for single attachments - if ($sessionKey === null && $this instanceof AttachOne) { - $this->delete(); + if ($sessionKey === null) { + $this->ensureAttachOneIsSingular(); } if (!array_key_exists('is_public', $model->attributes)) { @@ -134,9 +133,8 @@ public function save(Model $model, $sessionKey = null) */ public function create(array $attributes = [], $sessionKey = null) { - // Delete siblings for single attachments - if ($sessionKey === null && $this instanceof AttachOne) { - $this->delete(); + if ($sessionKey === null) { + $this->ensureAttachOneIsSingular(); } if (!array_key_exists('is_public', $attributes)) { @@ -181,10 +179,7 @@ public function add(Model $model, $sessionKey = null) return; } - // Delete siblings for single attachments - if ($this instanceof AttachOne) { - $this->delete(); - } + $this->ensureAttachOneIsSingular(); // Associate the model if ($this->parent->exists) { @@ -320,6 +315,17 @@ protected function isModelRemovable($model): bool $model->getAttribute('field') === $this->relationName; } + /** + * ensureAttachOneIsSingular ensures AttachOne only has one attachment, + * by deleting siblings for singular relations. + */ + protected function ensureAttachOneIsSingular() + { + if ($this instanceof AttachOne && $this->parent->exists) { + $this->delete(); + } + } + /** * isValidFileData returns true if the specified value can be used as the data attribute */ diff --git a/src/Database/Relations/HasMany.php b/src/Database/Relations/HasMany.php index ce2f18acb..d10f9e969 100644 --- a/src/Database/Relations/HasMany.php +++ b/src/Database/Relations/HasMany.php @@ -38,8 +38,8 @@ public function setSimpleValue($value) // Nulling the relationship if (!$value) { if ($this->parent->exists) { - $this->parent->bindEventOnce('model.afterSave', function () { - $this->update([$this->getForeignKeyName() => null]); + $this->parent->bindEventOnce('model.afterSave', function() { + $this->ensureRelationIsEmpty(); }); } return; diff --git a/src/Database/Relations/HasOne.php b/src/Database/Relations/HasOne.php index 97b57c2f3..57db078c6 100644 --- a/src/Database/Relations/HasOne.php +++ b/src/Database/Relations/HasOne.php @@ -40,8 +40,8 @@ public function setSimpleValue($value) // Nulling the relationship if (!$value) { if ($this->parent->exists) { - $this->parent->bindEventOnce('model.afterSave', function () { - $this->update([$this->getForeignKeyName() => null]); + $this->parent->bindEventOnce('model.afterSave', function() { + $this->ensureRelationIsEmpty(); }); } return; diff --git a/src/Database/Relations/HasOneOrMany.php b/src/Database/Relations/HasOneOrMany.php index bb4cd02cf..7591f7d19 100644 --- a/src/Database/Relations/HasOneOrMany.php +++ b/src/Database/Relations/HasOneOrMany.php @@ -205,6 +205,21 @@ protected function isModelRemovable($model): bool return ((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey()); } + /** + * ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled. + */ + protected function ensureRelationIsEmpty() + { + $options = $this->parent->getRelationDefinition($this->relationName); + + if (array_get($options, 'delete', false)) { + $this->delete(); + } + else { + $this->update([$this->getForeignKeyName() => null]); + } + } + /** * getRelatedKeyName * @return string diff --git a/src/Database/Relations/MorphMany.php b/src/Database/Relations/MorphMany.php index 702f35d71..cb1772499 100644 --- a/src/Database/Relations/MorphMany.php +++ b/src/Database/Relations/MorphMany.php @@ -39,10 +39,7 @@ public function setSimpleValue($value) if (!$value) { if ($this->parent->exists) { $this->parent->bindEventOnce('model.afterSave', function () { - $this->update([ - $this->getForeignKeyName() => null, - $this->getMorphType() => null - ]); + $this->ensureRelationIsEmpty(); }); } return; diff --git a/src/Database/Relations/MorphOne.php b/src/Database/Relations/MorphOne.php index 1872d66b7..0e6bbd25a 100644 --- a/src/Database/Relations/MorphOne.php +++ b/src/Database/Relations/MorphOne.php @@ -40,11 +40,8 @@ public function setSimpleValue($value) // Nulling the relationship if (!$value) { if ($this->parent->exists) { - $this->parent->bindEventOnce('model.afterSave', function () { - $this->update([ - $this->getForeignKeyName() => null, - $this->getMorphType() => null - ]); + $this->parent->bindEventOnce('model.afterSave', function() { + $this->ensureRelationIsEmpty(); }); } return; diff --git a/src/Database/Relations/MorphOneOrMany.php b/src/Database/Relations/MorphOneOrMany.php index be06ce788..d9d97815d 100644 --- a/src/Database/Relations/MorphOneOrMany.php +++ b/src/Database/Relations/MorphOneOrMany.php @@ -187,6 +187,24 @@ protected function isModelRemovable($model): bool $model->getAttribute($this->getMorphType()) === $this->morphClass; } + /** + * ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled. + */ + protected function ensureRelationIsEmpty() + { + $options = $this->parent->getRelationDefinition($this->relationName); + + if (array_get($options, 'delete', false)) { + $this->delete(); + } + else { + $this->update([ + $this->getForeignKeyName() => null, + $this->getMorphType() => null + ]); + } + } + /** * getRelatedKeyName * @return string diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index 2433f487e..fb0d3f7e7 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -47,15 +47,15 @@ public function initializeMultisite() $this->bindEvent('model.beforeSave', [$this, 'multisiteBeforeSave']); - $this->bindEvent('model.afterSave', [$this, 'multisiteAfterSave']); - $this->bindEvent('model.afterCreate', [$this, 'multisiteAfterCreate']); + $this->bindEvent('model.saveComplete', [$this, 'multisiteSaveComplete']); + $this->defineMultisiteRelations(); } /** - * multisiteBeforeSave constructor event + * multisiteBeforeSave constructor event used internally */ public function multisiteBeforeSave() { @@ -67,9 +67,9 @@ public function multisiteBeforeSave() } /** - * multisiteAfterSave constructor event + * multisiteSaveComplete constructor event used internally */ - public function multisiteAfterSave() + public function multisiteSaveComplete() { if ($this->getSaveOption('propagate') !== true) { return; @@ -101,7 +101,7 @@ public function multisiteAfterSave() } /** - * multisiteAfterCreate constructor event + * multisiteAfterCreate constructor event used internally */ public function multisiteAfterCreate() {