Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 82 additions & 5 deletions src/CascadeSoftDeletes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Date;

// Dummy class to hold the soft delete timestamp
// This is used to ensure that the timestamp is shared across all models
// that are using the CascadeSoftDeletes trait.
class CascadeSoftDeleteTimestamp {
static public $callerModel = null;
static public $softDeleteTimestamp = null;
}

trait CascadeSoftDeletes
{
use SoftDeletes;
/**
* Boot the trait.
*
Expand All @@ -19,8 +31,20 @@ protected static function bootCascadeSoftDeletes()
{
static::deleting(function ($model) {
$model->validateCascadingSoftDelete();
if (CascadeSoftDeleteTimestamp::$softDeleteTimestamp == null) {
CascadeSoftDeleteTimestamp::$softDeleteTimestamp = Date::now();
CascadeSoftDeleteTimestamp::$callerModel = $model;
$model->runCascadingDeletes();
}
else {
$model->runCascadingDeletes();
}
});

$model->runCascadingDeletes();
static::deleted(function ($model) {
if (!CascadeSoftDeleteTimestamp::$callerModel->is($model)) return;
CascadeSoftDeleteTimestamp::$softDeleteTimestamp = null;
CascadeSoftDeleteTimestamp::$callerModel = null;
});
}

Expand Down Expand Up @@ -64,7 +88,7 @@ protected function runCascadingDeletes()
protected function cascadeSoftDeletes($relationship)
{
$delete = $this->forceDeleting ? 'forceDelete' : 'delete';

$cb = function($model) use ($delete) {
isset($model->pivot) ? $model->pivot->{$delete}() : $model->{$delete}();
};
Expand All @@ -75,11 +99,30 @@ protected function cascadeSoftDeletes($relationship)
private function handleRecords($relationship, $cb)
{
$fetchMethod = $this->fetchMethod ?? 'get';
$model = $this->{$relationship}()->first();
$primary = $this->getRelationPrimaryKey($model);
$hasRelationships = $this->hasDescendantRelationships($model);
$delete = $this->forceDeleting ? 'forceDelete' : 'delete';

if ($fetchMethod == 'chunk') {
$this->{$relationship}()->chunk($this->chunkSize ?? 500, $cb);
while ($models = $this->{$relationship}()->select($primary)->limit($this->chunkSize ?? 500)->get()) {
if ($models->isEmpty()) {
break;
}
if (!$hasRelationships) {
$this->{$relationship}()->whereIn($primary, $models->pluck($primary))->{$delete}();
continue;
}
foreach($models as $model) {
$cb($model);
}
}
} else {
foreach($this->{$relationship}()->$fetchMethod() as $model) {
if (!$hasRelationships) {
$this->{$relationship}()->select($primary)->{$delete}();
return;
}
foreach($this->{$relationship}()->select($primary)->$fetchMethod() as $model) {
$cb($model);
}
}
Expand Down Expand Up @@ -129,10 +172,44 @@ protected function getCascadingDeletes()
*
* @return array
*/
protected function getActiveCascadingDeletes()
public function getActiveCascadingDeletes()
{
return array_filter($this->getCascadingDeletes(), function ($relationship) {
return $this->{$relationship}()->exists();
});
}

/**
* Get the primary key for the relationship model.
*
* @param Model $model
* @return string
*/
private function getRelationPrimaryKey($model) {
return $model->getKeyName();
}

/**
* Check if the model has any descendant relationships that also use cascading soft deletes.
*
* @param Model $model
* @return bool
*/
private function hasDescendantRelationships($model)
{
if (!method_exists($model, 'getActiveCascadingDeletes')) {
return false;
}
return count($model->getActiveCascadingDeletes()) > 0;
}

// Hacky way to sync the soft delete timestamp across models
// This is used to ensure that the soft delete timestamp is shared across all models and relationships
// that are using the CascadeSoftDeletes trait.
public function freshTimestamp() {
if (isset($this->syncTimestamp) && $this->syncTimestamp) {
return CascadeSoftDeleteTimestamp::$softDeleteTimestamp ?? Date::now();
}
return Date::now();
}
}