From 77b9f1b851bc3c5d0cf3bee5b206bf9382ba822f Mon Sep 17 00:00:00 2001 From: Gleb Date: Mon, 8 Aug 2016 12:36:23 +0400 Subject: [PATCH 01/11] Many2Many through pivot table support --- src/CascadeSoftDeletes.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CascadeSoftDeletes.php b/src/CascadeSoftDeletes.php index ce19dfd..9848afa 100644 --- a/src/CascadeSoftDeletes.php +++ b/src/CascadeSoftDeletes.php @@ -41,7 +41,12 @@ protected static function bootCascadeSoftDeletes() $model->{$relationship}->{$delete}(); } else { foreach ($model->{$relationship} as $child) { - $child->{$delete}(); + if ($child->pivot){ + $child->pivot->delete(); + } + else{ + $child->{$delete}(); + } } } } From 4ebf177c3ecf63d6b92b45c3bf04adbea9a1ff6c Mon Sep 17 00:00:00 2001 From: Gleb Date: Tue, 9 Aug 2016 10:15:59 +0400 Subject: [PATCH 02/11] Tests For Many2Many --- tests/CascadeSoftDeletesIntegrationTest.php | 55 +++++++++++++++++++++ tests/Entities/Author.php | 7 ++- tests/Entities/PostType.php | 5 ++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index 5830019..eb1fa51 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -50,6 +50,21 @@ public static function setupBeforeClass() $table->string('label'); $table->timestamps(); }); + + /** Many2Many (Authors <=> Posts Types) pivot table **/ + + $manager->schema()->create('authors__post_types', function ($table) { + + $table->increments('id'); + $table->integer('author_id'); + $table->integer('posttype_id'); + + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('author_id')->references('id')->on('author'); + $table->foreign('posttype_id')->references('id')->on('post_types'); + }); } @@ -68,6 +83,26 @@ public function it_cascades_deletes_when_deleting_a_parent_model() $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); } + /* Many2Many with pivot table(deletes entries in pivot) */ + public function it_cascades_deletes_entries_from_pivot_table() + { + $author = Tests\Entities\Author::create(['name' => 'ManyToManyTestAuthor']); + + $this->attachPostTypesToAuthor($author); + $this->assertCount(2, $author->posttypes); + + $author->delete(); + + $manager = Manager::$instance; + + $pivotCount = $manager->table('authors__post_types') + ->where('author_id', $author->id) + ->get() + ->count(); + + $this->assertCount(0, $pivotCount); + } + /** @test */ public function it_cascades_deletes_when_force_deleting_a_parent_model() { @@ -224,6 +259,26 @@ public function it_cascades_a_has_one_relationship() $this->assertCount(0, Tests\Entities\PostType::where('id', $type->id)->get()); } + /** + * Attach some post types to the given author. + * + * @return void + */ + + public function attachPostTypesToAuthor($author) + { + $author->posttypes()->saveMany([ + + Tests\Entities\PostType::create([ + 'label' => 'First Post Type', + ]), + + Tests\Entities\PostType::create([ + 'label' => 'Second Post Type', + ]) + ]); + } + /** * Attach some dummy posts (w/ comments) to the given author. * diff --git a/tests/Entities/Author.php b/tests/Entities/Author.php index 453ddd6..60811f3 100644 --- a/tests/Entities/Author.php +++ b/tests/Entities/Author.php @@ -12,7 +12,7 @@ class Author extends Model public $dates = ['deleted_at']; - protected $cascadeDeletes = ['posts']; + protected $cascadeDeletes = ['posts', 'posttypes']; protected $fillable = ['name']; @@ -20,4 +20,9 @@ public function posts() { return $this->hasMany('Tests\Entities\Post'); } + + public function posttypes() + { + return $this->belongsToMany('Tests\Entities\PostType', 'authors__post_types', 'author_id', 'posttype_id'); + } } diff --git a/tests/Entities/PostType.php b/tests/Entities/PostType.php index 777d9e7..cf51496 100644 --- a/tests/Entities/PostType.php +++ b/tests/Entities/PostType.php @@ -12,4 +12,9 @@ public function post() { return $this->belongsTo('Test\Entities\Post'); } + + public function authors() + { + return $this->belongsToMany('Tests\Entities\Author', 'authors__post_types', 'posttype_id', 'author_id'); + } } From 115151e43ee286798a5733bbff51c88d8c1de2ff Mon Sep 17 00:00:00 2001 From: Gleb Date: Tue, 9 Aug 2016 10:20:31 +0400 Subject: [PATCH 03/11] Whoops, bug --- tests/CascadeSoftDeletesIntegrationTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index eb1fa51..da2faa8 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -97,8 +97,7 @@ public function it_cascades_deletes_entries_from_pivot_table() $pivotCount = $manager->table('authors__post_types') ->where('author_id', $author->id) - ->get() - ->count(); + ->get(); $this->assertCount(0, $pivotCount); } From 13e3bdc3a90d831df770d4acbe1a81753d7e922f Mon Sep 17 00:00:00 2001 From: Gleb Date: Tue, 9 Aug 2016 10:21:46 +0400 Subject: [PATCH 04/11] Whoops, bug --- tests/CascadeSoftDeletesIntegrationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index da2faa8..d12e72d 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -95,11 +95,11 @@ public function it_cascades_deletes_entries_from_pivot_table() $manager = Manager::$instance; - $pivotCount = $manager->table('authors__post_types') + $pivotEntries = $manager->table('authors__post_types') ->where('author_id', $author->id) ->get(); - $this->assertCount(0, $pivotCount); + $this->assertCount(0, $pivotEntries); } /** @test */ From b47c17c6ecdf4b060ffd5ef0062023548d351398 Mon Sep 17 00:00:00 2001 From: Gleb Ivanov Date: Fri, 12 Aug 2016 08:20:53 +0000 Subject: [PATCH 05/11] detach() fix --- src/CascadeSoftDeletes.php | 10 +++++----- tests/CascadeSoftDeletesIntegrationTest.php | 15 ++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/CascadeSoftDeletes.php b/src/CascadeSoftDeletes.php index 9848afa..cf0b436 100644 --- a/src/CascadeSoftDeletes.php +++ b/src/CascadeSoftDeletes.php @@ -3,6 +3,7 @@ namespace Iatstuti\Database\Support; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; use LogicException; @@ -40,11 +41,10 @@ protected static function bootCascadeSoftDeletes() if ($model->{$relationship} instanceof Model) { $model->{$relationship}->{$delete}(); } else { - foreach ($model->{$relationship} as $child) { - if ($child->pivot){ - $child->pivot->delete(); - } - else{ + if ($model->{$relationship}() instanceof BelongsToMany) { + $model->{$relationship}()->detach(); + } else { + foreach ($model->{$relationship} as $child) { $child->{$delete}(); } } diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index d12e72d..0f40681 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -51,16 +51,12 @@ public static function setupBeforeClass() $table->timestamps(); }); - /** Many2Many (Authors <=> Posts Types) pivot table **/ - $manager->schema()->create('authors__post_types', function ($table) { $table->increments('id'); $table->integer('author_id'); $table->integer('posttype_id'); - $table->timestamps(); - $table->softDeletes(); $table->foreign('author_id')->references('id')->on('author'); $table->foreign('posttype_id')->references('id')->on('post_types'); @@ -83,7 +79,7 @@ public function it_cascades_deletes_when_deleting_a_parent_model() $this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get()); } - /* Many2Many with pivot table(deletes entries in pivot) */ + /** @test */ public function it_cascades_deletes_entries_from_pivot_table() { $author = Tests\Entities\Author::create(['name' => 'ManyToManyTestAuthor']); @@ -93,13 +89,11 @@ public function it_cascades_deletes_entries_from_pivot_table() $author->delete(); - $manager = Manager::$instance; - - $pivotEntries = $manager->table('authors__post_types') - ->where('author_id', $author->id) - ->get(); + $pivotEntries = $author->withThrashed()->posttypes; $this->assertCount(0, $pivotEntries); + + $author->forceDelete(); } /** @test */ @@ -263,7 +257,6 @@ public function it_cascades_a_has_one_relationship() * * @return void */ - public function attachPostTypesToAuthor($author) { $author->posttypes()->saveMany([ From cb624854dfbf86d059c3c867de4c60ff167614a1 Mon Sep 17 00:00:00 2001 From: Gleb Ivanov Date: Fri, 12 Aug 2016 08:38:53 +0000 Subject: [PATCH 06/11] Integration test fix --- tests/CascadeSoftDeletesIntegrationTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index 0f40681..fd86edb 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -89,11 +89,11 @@ public function it_cascades_deletes_entries_from_pivot_table() $author->delete(); - $pivotEntries = $author->withThrashed()->posttypes; + $pivotEntries = Manager::table('authors__post_types') + ->where('author_id', $author->id) + ->get(); $this->assertCount(0, $pivotEntries); - - $author->forceDelete(); } /** @test */ From 3356455b434c0a933c3074771ffd0768671c4fbe Mon Sep 17 00:00:00 2001 From: Michael Dyrynda Date: Tue, 6 Sep 2016 19:51:01 +0930 Subject: [PATCH 07/11] Extract exception formatting to static methods in exception class --- src/CascadeSoftDeleteException.php | 23 +++++++++++++++++++++ src/CascadeSoftDeletes.php | 12 ++--------- tests/CascadeSoftDeletesIntegrationTest.php | 11 +++++----- 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/CascadeSoftDeleteException.php diff --git a/src/CascadeSoftDeleteException.php b/src/CascadeSoftDeleteException.php new file mode 100644 index 0000000..b18788e --- /dev/null +++ b/src/CascadeSoftDeleteException.php @@ -0,0 +1,23 @@ +implementsSoftDeletes()) { - throw new LogicException(sprintf( - '%s does not implement Illuminate\Database\Eloquent\SoftDeletes', - get_called_class() - )); + throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class()); } if ($invalidCascadingRelationships = $model->hasInvalidCascadingRelationships()) { - throw new LogicException(sprintf( - '%s [%s] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation', - str_plural('Relationship', count($invalidCascadingRelationships)), - join(', ', $invalidCascadingRelationships) - )); + throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingRelationships); } $delete = $model->forceDeleting ? 'forceDelete' : 'delete'; diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index fd86edb..dcf7189 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -1,5 +1,6 @@ delete(); - } catch (\LogicException $e) { + } catch (CascadeSoftDeleteException $e) { $this->assertNotNull(Tests\Entities\InvalidRelationshipPost::find($post->id)); $this->assertCount(3, Tests\Entities\Comment::where('post_id', $post->id)->get()); } @@ -183,7 +184,7 @@ public function it_can_accept_cascade_deletes_as_a_single_string() /** * @test - * @expectedException \LogicException + * @expectedException \Iatstuti\Database\Support\CascadeSoftDeleteException * @expectedExceptionMessageRegExp /Relationship \[.*\] must exist and return an object of type Illuminate\\Database\\Eloquent\\Relations\\Relation/ */ public function it_handles_situations_where_the_relationship_method_does_not_exist() @@ -267,7 +268,7 @@ public function attachPostTypesToAuthor($author) Tests\Entities\PostType::create([ 'label' => 'Second Post Type', - ]) + ]), ]); } From fb2538866856ca895f681c671246b503fcd0f4f4 Mon Sep 17 00:00:00 2001 From: Michael Dyrynda Date: Thu, 8 Sep 2016 23:19:45 +0930 Subject: [PATCH 08/11] Refactor processing of cascaded soft deletes Resolves #13 I've broken the functionality out into separate methods and hopefully tidied this package up a little in the process. I've tweaked the behaviour within the (old) static deleting method in order to handle many-to-many relationships, where a pivot table is present. Whilst this works and the failing test supplied now passes, it should be noted that this can actually be quite database heavy, as we iterate through each relationship and delete its records recursively. More records, more delete queries. I'm open to anybody having a better suggestion to handling this. I've spent quite a while on it and come to the conclusion that there's a really good reason this stuff is implemented at the database level. If you're getting performance issues, it might be time to move away from soft deletes for this use-case. For smaller datasets, it ought to be "ok". --- src/CascadeSoftDeletes.php | 72 ++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/src/CascadeSoftDeletes.php b/src/CascadeSoftDeletes.php index d98bef6..e6cd300 100644 --- a/src/CascadeSoftDeletes.php +++ b/src/CascadeSoftDeletes.php @@ -19,33 +19,59 @@ trait CascadeSoftDeletes protected static function bootCascadeSoftDeletes() { static::deleting(function ($model) { - if (! $model->implementsSoftDeletes()) { - throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class()); - } - - if ($invalidCascadingRelationships = $model->hasInvalidCascadingRelationships()) { - throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingRelationships); - } - - $delete = $model->forceDeleting ? 'forceDelete' : 'delete'; - - foreach ($model->getActiveCascadingDeletes() as $relationship) { - if ($model->{$relationship} instanceof Model) { - $model->{$relationship}->{$delete}(); - } else { - if ($model->{$relationship}() instanceof BelongsToMany) { - $model->{$relationship}()->detach(); - } else { - foreach ($model->{$relationship} as $child) { - $child->{$delete}(); - } - } - } - } + $model->validateCascadingSoftDelete(); + + $model->runCascadingDeletes(); }); } + /** + * Validate that the calling model is correctly setup for cascading soft deletes. + * + * @throws \Iatstuti\Database\Support\CascadeSoftDeleteException + */ + protected function validateCascadingSoftDelete() + { + if (! $this->implementsSoftDeletes()) { + throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class()); + } + + if ($invalidCascadingRelationships = $this->hasInvalidCascadingRelationships()) { + throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingRelationships); + } + } + + + /** + * Run the cascading soft delete for this model. + * + * @return void + */ + protected function runCascadingDeletes() + { + foreach ($this->getActiveCascadingDeletes() as $relationship) { + $this->cascadeSoftDeletes($relationship); + } + } + + + /** + * Cascade delete the given relationship on the given mode. + * + * @param string $relationship + * @return return + */ + protected function cascadeSoftDeletes($relationship) + { + $delete = $this->forceDeleting ? 'forceDelete' : 'delete'; + + foreach ($this->{$relationship}()->get() as $model) { + $model->pivot ? $model->pivot->{$delete}() : $model->{$delete}(); + } + } + + /** * Determine if the current model implements soft deletes. * From 82d670a2bdc40079e73061a338d5a945ee7eb85d Mon Sep 17 00:00:00 2001 From: Michael Dyrynda Date: Thu, 8 Sep 2016 23:23:17 +0930 Subject: [PATCH 09/11] whitespace --- tests/CascadeSoftDeletesIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CascadeSoftDeletesIntegrationTest.php b/tests/CascadeSoftDeletesIntegrationTest.php index dcf7189..61334a6 100644 --- a/tests/CascadeSoftDeletesIntegrationTest.php +++ b/tests/CascadeSoftDeletesIntegrationTest.php @@ -92,7 +92,7 @@ public function it_cascades_deletes_entries_from_pivot_table() $pivotEntries = Manager::table('authors__post_types') ->where('author_id', $author->id) - ->get(); + ->get(); $this->assertCount(0, $pivotEntries); } From 560e34e14eb1a32d10366572084e78a3596ab2e9 Mon Sep 17 00:00:00 2001 From: Michael Dyrynda Date: Fri, 6 Sep 2019 14:43:40 +0930 Subject: [PATCH 10/11] update to use Str class --- src/CascadeSoftDeleteException.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CascadeSoftDeleteException.php b/src/CascadeSoftDeleteException.php index b18788e..53fbc12 100644 --- a/src/CascadeSoftDeleteException.php +++ b/src/CascadeSoftDeleteException.php @@ -3,6 +3,7 @@ namespace Iatstuti\Database\Support; use Exception; +use Illuminate\Support\Str; class CascadeSoftDeleteException extends Exception { @@ -16,7 +17,7 @@ public static function invalidRelationships($relationships) { return new static(sprintf( '%s [%s] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation', - str_plural('Relationship', count($relationships)), + Str::plural('Relationship', count($relationships)), join(', ', $relationships) )); } From 820370f6e8adf5e68bcd9029f0c161e59c256f05 Mon Sep 17 00:00:00 2001 From: Michael Dyrynda Date: Fri, 6 Sep 2019 14:43:54 +0930 Subject: [PATCH 11/11] ignore composer.lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 57872d0..3a9875b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /vendor/ +composer.lock