Skip to content

Commit

Permalink
Move replication logic to db.replicator
Browse files Browse the repository at this point in the history
This gives more room for logic and reduces the memory footprint
  • Loading branch information
daftspunk committed Feb 14, 2024
1 parent d915d72 commit 87ff998
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 87 deletions.
91 changes: 5 additions & 86 deletions src/Database/Concerns/HasReplication.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<?php namespace October\Rain\Database\Concerns;

use October\Rain\Support\Arr;
use Illuminate\Database\Eloquent\Model as EloquentModel;
use Illuminate\Database\Eloquent\Collection as CollectionBase;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
use App;

/**
* HasReplication for a model
Expand All @@ -19,7 +16,7 @@ trait HasReplication
*/
public function replicateWithRelations(array $except = null)
{
return $this->replicateRelationsInternal($except);
return App::makeWith('db.replicator', ['model' => $this])->replicate($except);
}

/**
Expand All @@ -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'];
}
}
4 changes: 3 additions & 1 deletion src/Database/DatabaseServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand All @@ -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"];
}
}
153 changes: 153 additions & 0 deletions src/Database/Replicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php namespace October\Rain\Database;

use October\Rain\Support\Arr;
use Illuminate\Database\Eloquent\Model as EloquentModel;
use Illuminate\Database\Eloquent\Collection as CollectionBase;
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;

/**
* Replicator service to duplicating and replicating model records
*/
class Replicator
{
/**
* @var \Model model context
*/
protected $model;

/**
* @var bool isDuplicating the record or just returning a non-existing instance
*/
protected $isDuplicating = false;

/**
* @var bool isMultisite context
*/
protected $isMultisite = false;

/**
* __construct
*/
public function __construct($model)
{
$this->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'];
}
}

0 comments on commit 87ff998

Please sign in to comment.