From 87ff998b398555daa860665b51c8472b4a50800c Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 14 Feb 2024 13:05:18 +1100 Subject: [PATCH] Move replication logic to db.replicator This gives more room for logic and reduces the memory footprint --- src/Database/Concerns/HasReplication.php | 91 +------------- src/Database/DatabaseServiceProvider.php | 4 +- src/Database/Replicator.php | 153 +++++++++++++++++++++++ 3 files changed, 161 insertions(+), 87 deletions(-) create mode 100644 src/Database/Replicator.php diff --git a/src/Database/Concerns/HasReplication.php b/src/Database/Concerns/HasReplication.php index b9c6422c2..80dbdc4bd 100644 --- a/src/Database/Concerns/HasReplication.php +++ b/src/Database/Concerns/HasReplication.php @@ -1,9 +1,6 @@ replicateRelationsInternal($except); + return App::makeWith('db.replicator', ['model' => $this])->replicate($except); } /** @@ -32,98 +29,20 @@ public function replicateWithRelations(array $except = null) */ public function duplicateWithRelations(array $except = null) { - return $this->replicateRelationsInternal($except, ['isDuplicate' => true]); + return App::makeWith('db.replicator', ['model' => $this])->duplicate($except); } /** - * replicateRelationsInternal + * newReplicationInstance returns a new instance used by the replicator */ - protected function replicateRelationsInternal(array $except = null, array $options = []) + public function newReplicationInstance($attributes) { - 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 - ); - $instance = $this->newInstance(); $instance->setRawAttributes($attributes); $instance->fireModelEvent('replicating', false); - $definitions = $this->getRelationDefinitions(); - - foreach ($definitions as $type => $relations) { - foreach ($relations as $name => $options) { - if ($this->isRelationReplicable($name, $isMultisite, $isDuplicate)) { - $this->replicateRelationInternal($instance->$name(), $this->$name); - } - } - } - return $instance; } - - /** - * replicateRelationInternal on the model instance with the supplied ones - */ - protected function replicateRelationInternal($relationObject, $models) - { - if ($models instanceof CollectionBase) { - $models = $models->all(); - } - elseif ($models instanceof EloquentModel) { - $models = [$models]; - } - else { - $models = (array) $models; - } - - foreach (array_filter($models) as $model) { - if ($relationObject instanceof HasOneOrMany) { - $relationObject->add($model->replicateWithRelations()); - } - else { - $relationObject->add($model); - } - } - } - - /** - * 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 $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; - } - - return (bool) $definition['replicate']; - } } diff --git a/src/Database/DatabaseServiceProvider.php b/src/Database/DatabaseServiceProvider.php index 3defd800b..78520b6e3 100644 --- a/src/Database/DatabaseServiceProvider.php +++ b/src/Database/DatabaseServiceProvider.php @@ -72,6 +72,8 @@ protected function registerConnectionServices() return new DatabaseTransactionsManager; }); + $this->app->bind('db.replicator', Replicator::class); + $this->app->singleton('db.dongle', function ($app) { return new Dongle($this->getDefaultDatabaseDriver(), $app['db']); }); @@ -84,6 +86,6 @@ protected function getDefaultDatabaseDriver(): string { $defaultConnection = $this->app['db']->getDefaultConnection(); - return $this->app['config']['database.connections.' . $defaultConnection . '.driver']; + return $this->app['config']["database.connections.{$defaultConnection}.driver"]; } } diff --git a/src/Database/Replicator.php b/src/Database/Replicator.php new file mode 100644 index 000000000..1e0a998c7 --- /dev/null +++ b/src/Database/Replicator.php @@ -0,0 +1,153 @@ +model = $model; + $this->isMultisite = $model->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class); + } + + /** + * replicate replicates the model into a new, non-existing instance, + * including replicating relations. + * + * @param array|null $except + * @return static + */ + public function replicate(array $except = null) + { + $this->isDuplicating = false; + + return $this->replicateRelationsInternal($except); + } + + /** + * duplicate 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 duplicate(array $except = null) + { + $this->isDuplicating = true; + + return $this->replicateRelationsInternal($except); + } + + /** + * replicateRelationsInternal + */ + protected function replicateRelationsInternal(array $except = null) + { + $defaults = [ + $this->model->getKeyName(), + $this->model->getCreatedAtColumn(), + $this->model->getUpdatedAtColumn(), + ]; + + if ($this->isMultisite) { + $defaults[] = 'site_root_id'; + } + + $attributes = Arr::except( + $this->model->attributes, + $except ? array_unique(array_merge($except, $defaults)) : $defaults + ); + + $instance = $this->model->newReplicationInstance($attributes); + + $definitions = $this->model->getRelationDefinitions(); + + foreach ($definitions as $type => $relations) { + foreach ($relations as $name => $options) { + if ($this->isRelationReplicable($name)) { + $this->replicateRelationInternal($instance->$name(), $this->model->$name); + } + } + } + + return $instance; + } + + /** + * replicateRelationInternal on the model instance with the supplied ones + */ + protected function replicateRelationInternal($relationObject, $models) + { + if ($models instanceof CollectionBase) { + $models = $models->all(); + } + elseif ($models instanceof EloquentModel) { + $models = [$models]; + } + else { + $models = (array) $models; + } + + foreach (array_filter($models) as $model) { + if ($relationObject instanceof HasOneOrMany) { + $relationObject->add($model->replicateWithRelations()); + } + else { + $relationObject->add($model); + } + } + } + + /** + * 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 + { + $relationType = $this->model->getRelationType($name); + if ($relationType === 'morphTo') { + return false; + } + + // Relation is shared via propagation + if ( + !$this->isDuplicating && + $this->isMultisite && + $this->model->isAttributePropagatable($name) + ) { + return false; + } + + $definition = $this->model->getRelationDefinition($name); + if (!array_key_exists('replicate', $definition)) { + return true; + } + + return (bool) $definition['replicate']; + } +}