Skip to content

Commit a5a16c7

Browse files
Merge pull request #34 from michaeldyrynda/pr-13
Add many to many support
2 parents dfb4216 + 820370f commit a5a16c7

File tree

6 files changed

+139
-31
lines changed

6 files changed

+139
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/vendor/
2+
composer.lock

src/CascadeSoftDeleteException.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Iatstuti\Database\Support;
4+
5+
use Exception;
6+
use Illuminate\Support\Str;
7+
8+
class CascadeSoftDeleteException extends Exception
9+
{
10+
public static function softDeleteNotImplemented($class)
11+
{
12+
return new static(sprintf('%s does not implement Illuminate\Database\Eloquent\SoftDeletes', $class));
13+
}
14+
15+
16+
public static function invalidRelationships($relationships)
17+
{
18+
return new static(sprintf(
19+
'%s [%s] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation',
20+
Str::plural('Relationship', count($relationships)),
21+
join(', ', $relationships)
22+
));
23+
}
24+
}

src/CascadeSoftDeletes.php

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use LogicException;
66
use Illuminate\Support\Str;
77
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
89
use Illuminate\Database\Eloquent\Relations\Relation;
910

1011
trait CascadeSoftDeletes
@@ -20,36 +21,59 @@ trait CascadeSoftDeletes
2021
protected static function bootCascadeSoftDeletes()
2122
{
2223
static::deleting(function ($model) {
23-
if (! $model->implementsSoftDeletes()) {
24-
throw new LogicException(sprintf(
25-
'%s does not implement Illuminate\Database\Eloquent\SoftDeletes',
26-
get_called_class()
27-
));
28-
}
29-
30-
if ($invalidCascadingRelationships = $model->hasInvalidCascadingRelationships()) {
31-
throw new LogicException(sprintf(
32-
'%s [%s] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation',
33-
Str::plural('Relationship', count($invalidCascadingRelationships)),
34-
join(', ', $invalidCascadingRelationships)
35-
));
36-
}
37-
38-
$delete = $model->forceDeleting ? 'forceDelete' : 'delete';
39-
40-
foreach ($model->getActiveCascadingDeletes() as $relationship) {
41-
if ($model->{$relationship} instanceof Model) {
42-
$model->{$relationship}->{$delete}();
43-
} else {
44-
foreach ($model->{$relationship} as $child) {
45-
$child->{$delete}();
46-
}
47-
}
48-
}
24+
$model->validateCascadingSoftDelete();
25+
26+
$model->runCascadingDeletes();
4927
});
5028
}
5129

5230

31+
/**
32+
* Validate that the calling model is correctly setup for cascading soft deletes.
33+
*
34+
* @throws \Iatstuti\Database\Support\CascadeSoftDeleteException
35+
*/
36+
protected function validateCascadingSoftDelete()
37+
{
38+
if (! $this->implementsSoftDeletes()) {
39+
throw CascadeSoftDeleteException::softDeleteNotImplemented(get_called_class());
40+
}
41+
42+
if ($invalidCascadingRelationships = $this->hasInvalidCascadingRelationships()) {
43+
throw CascadeSoftDeleteException::invalidRelationships($invalidCascadingRelationships);
44+
}
45+
}
46+
47+
48+
/**
49+
* Run the cascading soft delete for this model.
50+
*
51+
* @return void
52+
*/
53+
protected function runCascadingDeletes()
54+
{
55+
foreach ($this->getActiveCascadingDeletes() as $relationship) {
56+
$this->cascadeSoftDeletes($relationship);
57+
}
58+
}
59+
60+
61+
/**
62+
* Cascade delete the given relationship on the given mode.
63+
*
64+
* @param string $relationship
65+
* @return return
66+
*/
67+
protected function cascadeSoftDeletes($relationship)
68+
{
69+
$delete = $this->forceDeleting ? 'forceDelete' : 'delete';
70+
71+
foreach ($this->{$relationship}()->get() as $model) {
72+
$model->pivot ? $model->pivot->{$delete}() : $model->{$delete}();
73+
}
74+
}
75+
76+
5377
/**
5478
* Determine if the current model implements soft deletes.
5579
*

tests/CascadeSoftDeletesIntegrationTest.php

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use PHPUnit\Framework\TestCase;
44
use Illuminate\Events\Dispatcher;
5+
use Iatstuti\Database\Support\CascadeSoftDeleteException;
56
use Illuminate\Container\Container;
67
use Illuminate\Database\Capsule\Manager;
78

@@ -49,6 +50,17 @@ public static function setupBeforeClass(): void
4950
$table->string('label');
5051
$table->timestamps();
5152
});
53+
54+
$manager->schema()->create('authors__post_types', function ($table) {
55+
56+
$table->increments('id');
57+
$table->integer('author_id');
58+
$table->integer('posttype_id');
59+
$table->timestamps();
60+
61+
$table->foreign('author_id')->references('id')->on('author');
62+
$table->foreign('posttype_id')->references('id')->on('post_types');
63+
});
5264
}
5365

5466

@@ -67,6 +79,23 @@ public function it_cascades_deletes_when_deleting_a_parent_model()
6779
$this->assertCount(0, Tests\Entities\Comment::where('post_id', $post->id)->get());
6880
}
6981

82+
/** @test */
83+
public function it_cascades_deletes_entries_from_pivot_table()
84+
{
85+
$author = Tests\Entities\Author::create(['name' => 'ManyToManyTestAuthor']);
86+
87+
$this->attachPostTypesToAuthor($author);
88+
$this->assertCount(2, $author->posttypes);
89+
90+
$author->delete();
91+
92+
$pivotEntries = Manager::table('authors__post_types')
93+
->where('author_id', $author->id)
94+
->get();
95+
96+
$this->assertCount(0, $pivotEntries);
97+
}
98+
7099
/** @test */
71100
public function it_cascades_deletes_when_force_deleting_a_parent_model()
72101
{
@@ -88,7 +117,7 @@ public function it_cascades_deletes_when_force_deleting_a_parent_model()
88117
*/
89118
public function it_takes_exception_to_models_that_do_not_implement_soft_deletes()
90119
{
91-
$this->expectException(LogicException::class);
120+
$this->expectException(CascadeSoftDeleteException::class);
92121
$this->expectExceptionMessage('Tests\Entities\NonSoftDeletingPost does not implement Illuminate\Database\Eloquent\SoftDeletes');
93122

94123
$post = Tests\Entities\NonSoftDeletingPost::create([
@@ -106,7 +135,7 @@ public function it_takes_exception_to_models_that_do_not_implement_soft_deletes(
106135
*/
107136
public function it_takes_exception_to_models_trying_to_cascade_deletes_on_invalid_relationships()
108137
{
109-
$this->expectException(LogicException::class);
138+
$this->expectException(CascadeSoftDeleteException::class);
110139
$this->expectExceptionMessage('Relationships [invalidRelationship, anotherInvalidRelationship] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation');
111140

112141
$post = Tests\Entities\InvalidRelationshipPost::create([
@@ -131,7 +160,7 @@ public function it_ensures_that_no_deletes_are_performed_if_there_are_invalid_re
131160

132161
try {
133162
$post->delete();
134-
} catch (LogicException $e) {
163+
} catch (CascadeSoftDeleteException $e) {
135164
$this->assertNotNull(Tests\Entities\InvalidRelationshipPost::find($post->id));
136165
$this->assertCount(3, Tests\Entities\Comment::where('post_id', $post->id)->get());
137166
}
@@ -156,10 +185,11 @@ public function it_can_accept_cascade_deletes_as_a_single_string()
156185

157186
/**
158187
* @test
188+
159189
*/
160190
public function it_handles_situations_where_the_relationship_method_does_not_exist()
161191
{
162-
$this->expectException(LogicException::class);
192+
$this->expectException(CascadeSoftDeleteException::class);
163193
$this->expectExceptionMessage('Relationship [comments] must exist and return an object of type Illuminate\Database\Eloquent\Relations\Relation');
164194

165195
$post = Tests\Entities\PostWithMissingRelationshipMethod::create([
@@ -226,6 +256,25 @@ public function it_cascades_a_has_one_relationship()
226256
$this->assertCount(0, Tests\Entities\PostType::where('id', $type->id)->get());
227257
}
228258

259+
/**
260+
* Attach some post types to the given author.
261+
*
262+
* @return void
263+
*/
264+
public function attachPostTypesToAuthor($author)
265+
{
266+
$author->posttypes()->saveMany([
267+
268+
Tests\Entities\PostType::create([
269+
'label' => 'First Post Type',
270+
]),
271+
272+
Tests\Entities\PostType::create([
273+
'label' => 'Second Post Type',
274+
]),
275+
]);
276+
}
277+
229278
/**
230279
* Attach some dummy posts (w/ comments) to the given author.
231280
*

tests/Entities/Author.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ class Author extends Model
1212

1313
public $dates = ['deleted_at'];
1414

15-
protected $cascadeDeletes = ['posts'];
15+
protected $cascadeDeletes = ['posts', 'posttypes'];
1616

1717
protected $fillable = ['name'];
1818

1919
public function posts()
2020
{
2121
return $this->hasMany('Tests\Entities\Post');
2222
}
23+
24+
public function posttypes()
25+
{
26+
return $this->belongsToMany('Tests\Entities\PostType', 'authors__post_types', 'author_id', 'posttype_id');
27+
}
2328
}

tests/Entities/PostType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ public function post()
1212
{
1313
return $this->belongsTo('Test\Entities\Post');
1414
}
15+
16+
public function authors()
17+
{
18+
return $this->belongsToMany('Tests\Entities\Author', 'authors__post_types', 'posttype_id', 'author_id');
19+
}
1520
}

0 commit comments

Comments
 (0)