Skip to content

Commit 8fcc7cc

Browse files
authored
[12.x] Fix model pruning when non model files are in the same directory (#56071)
* Allow using the `--path` parameter in `PruneCommandTest` * Add broken example * Filter out non Eloquent model classes * Break example with abstract model * Expand string assertion * Exclude abstract models from pruning
1 parent 46b66aa commit 8fcc7cc

10 files changed

+189
-81
lines changed

src/Illuminate/Database/Console/PruneCommand.php

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

55
use Illuminate\Console\Command;
66
use Illuminate\Contracts\Events\Dispatcher;
7+
use Illuminate\Database\Eloquent\Model;
78
use Illuminate\Database\Events\ModelPruningFinished;
89
use Illuminate\Database\Events\ModelPruningStarting;
910
use Illuminate\Database\Events\ModelsPruned;
@@ -137,8 +138,7 @@ protected function models()
137138
);
138139
})
139140
->when(! empty($except), fn ($models) => $models->reject(fn ($model) => in_array($model, $except)))
140-
->filter(fn ($model) => class_exists($model))
141-
->filter(fn ($model) => $model::isPrunable())
141+
->filter(fn ($model) => $this->isPrunable($model))
142142
->values();
143143
}
144144

@@ -179,4 +179,18 @@ protected function pretendToPrune($model)
179179
$this->components->info("{$count} [{$model}] records will be pruned.");
180180
}
181181
}
182+
183+
/**
184+
* Determine if the given model is prunable.
185+
*
186+
* @param string $model
187+
* @return bool
188+
*/
189+
private function isPrunable(string $model)
190+
{
191+
return class_exists($model)
192+
&& is_a($model, Model::class, true)
193+
&& ! (new \ReflectionClass($model))->isAbstract()
194+
&& $model::isPrunable();
195+
}
182196
}

tests/Database/PruneCommandTest.php

Lines changed: 51 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
77
use Illuminate\Database\Capsule\Manager as DB;
88
use Illuminate\Database\Console\PruneCommand;
9-
use Illuminate\Database\Eloquent\MassPrunable;
10-
use Illuminate\Database\Eloquent\Model;
11-
use Illuminate\Database\Eloquent\Prunable;
12-
use Illuminate\Database\Eloquent\SoftDeletes;
139
use Illuminate\Database\Events\ModelPruningFinished;
1410
use Illuminate\Database\Events\ModelPruningStarting;
1511
use Illuminate\Database\Events\ModelsPruned;
@@ -26,7 +22,15 @@ protected function setUp(): void
2622
{
2723
parent::setUp();
2824

29-
Application::setInstance($container = new Application);
25+
Application::setInstance($container = new Application(__DIR__.'/Pruning'));
26+
27+
Closure::bind(
28+
fn () => $this->namespace = 'Illuminate\\Tests\\Database\\Pruning\\',
29+
$container,
30+
Application::class,
31+
)();
32+
33+
$container->useAppPath(__DIR__.'/Pruning');
3034

3135
$container->singleton(DispatcherContract::class, function () {
3236
return new Dispatcher();
@@ -37,12 +41,12 @@ protected function setUp(): void
3741

3842
public function testPrunableModelWithPrunableRecords()
3943
{
40-
$output = $this->artisan(['--model' => PrunableTestModelWithPrunableRecords::class]);
44+
$output = $this->artisan(['--model' => Pruning\Models\PrunableTestModelWithPrunableRecords::class]);
4145

4246
$output = $output->fetch();
4347

4448
$this->assertStringContainsString(
45-
'Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords',
49+
'Illuminate\Tests\Database\Pruning\Models\PrunableTestModelWithPrunableRecords',
4650
$output,
4751
);
4852

@@ -52,7 +56,7 @@ public function testPrunableModelWithPrunableRecords()
5256
);
5357

5458
$this->assertStringContainsString(
55-
'Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords',
59+
'Illuminate\Tests\Database\Pruning\Models\PrunableTestModelWithPrunableRecords',
5660
$output,
5761
);
5862

@@ -64,10 +68,10 @@ public function testPrunableModelWithPrunableRecords()
6468

6569
public function testPrunableTestModelWithoutPrunableRecords()
6670
{
67-
$output = $this->artisan(['--model' => PrunableTestModelWithoutPrunableRecords::class]);
71+
$output = $this->artisan(['--model' => Pruning\Models\PrunableTestModelWithoutPrunableRecords::class]);
6872

6973
$this->assertStringContainsString(
70-
'No prunable [Illuminate\Tests\Database\PrunableTestModelWithoutPrunableRecords] records found.',
74+
'No prunable [Illuminate\Tests\Database\Pruning\Models\PrunableTestModelWithoutPrunableRecords] records found.',
7175
$output->fetch()
7276
);
7377
}
@@ -92,12 +96,12 @@ public function testPrunableSoftDeletedModelWithPrunableRecords()
9296
['value' => 4, 'deleted_at' => '2021-12-02 00:00:00'],
9397
]);
9498

95-
$output = $this->artisan(['--model' => PrunableTestSoftDeletedModelWithPrunableRecords::class]);
99+
$output = $this->artisan(['--model' => Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords::class]);
96100

97101
$output = $output->fetch();
98102

99103
$this->assertStringContainsString(
100-
'Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords',
104+
'Illuminate\Tests\Database\Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords',
101105
$output,
102106
);
103107

@@ -106,29 +110,51 @@ public function testPrunableSoftDeletedModelWithPrunableRecords()
106110
$output,
107111
);
108112

109-
$this->assertEquals(2, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count());
113+
$this->assertEquals(2, Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count());
110114
}
111115

112116
public function testNonPrunableTest()
113117
{
114-
$output = $this->artisan(['--model' => NonPrunableTestModel::class]);
118+
$output = $this->artisan(['--model' => Pruning\Models\NonPrunableTestModel::class]);
115119

116120
$this->assertStringContainsString(
117-
'No prunable [Illuminate\Tests\Database\NonPrunableTestModel] records found.',
121+
'No prunable [Illuminate\Tests\Database\Pruning\Models\NonPrunableTestModel] records found.',
118122
$output->fetch(),
119123
);
120124
}
121125

122126
public function testNonPrunableTestWithATrait()
123127
{
124-
$output = $this->artisan(['--model' => NonPrunableTrait::class]);
128+
$output = $this->artisan(['--model' => Pruning\Models\NonPrunableTrait::class]);
125129

126130
$this->assertStringContainsString(
127131
'No prunable models found.',
128132
$output->fetch(),
129133
);
130134
}
131135

136+
public function testNonModelFilesAreIgnoredTest()
137+
{
138+
$output = $this->artisan(['--path' => 'Models']);
139+
140+
$output = $output->fetch();
141+
142+
$this->assertStringNotContainsString(
143+
'No prunable [Illuminate\Tests\Database\Pruning\Models\AbstractPrunableModel] records found.',
144+
$output,
145+
);
146+
147+
$this->assertStringNotContainsString(
148+
'No prunable [Illuminate\Tests\Database\Pruning\Models\SomeClass] records found.',
149+
$output,
150+
);
151+
152+
$this->assertStringNotContainsString(
153+
'No prunable [Illuminate\Tests\Database\Pruning\Models\SomeEnum] records found.',
154+
$output,
155+
);
156+
}
157+
132158
public function testTheCommandMayBePretended()
133159
{
134160
$db = new DB;
@@ -151,16 +177,16 @@ public function testTheCommandMayBePretended()
151177
]);
152178

153179
$output = $this->artisan([
154-
'--model' => PrunableTestModelWithPrunableRecords::class,
180+
'--model' => Pruning\Models\PrunableTestModelWithPrunableRecords::class,
155181
'--pretend' => true,
156182
]);
157183

158184
$this->assertStringContainsString(
159-
'3 [Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords] records will be pruned.',
185+
'3 [Illuminate\Tests\Database\Pruning\Models\PrunableTestModelWithPrunableRecords] records will be pruned.',
160186
$output->fetch(),
161187
);
162188

163-
$this->assertEquals(5, PrunableTestModelWithPrunableRecords::count());
189+
$this->assertEquals(5, Pruning\Models\PrunableTestModelWithPrunableRecords::count());
164190
}
165191

166192
public function testTheCommandMayBePretendedOnSoftDeletedModel()
@@ -184,16 +210,16 @@ public function testTheCommandMayBePretendedOnSoftDeletedModel()
184210
]);
185211

186212
$output = $this->artisan([
187-
'--model' => PrunableTestSoftDeletedModelWithPrunableRecords::class,
213+
'--model' => Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords::class,
188214
'--pretend' => true,
189215
]);
190216

191217
$this->assertStringContainsString(
192-
'2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records will be pruned.',
218+
'2 [Illuminate\Tests\Database\Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords] records will be pruned.',
193219
$output->fetch(),
194220
);
195221

196-
$this->assertEquals(4, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count());
222+
$this->assertEquals(4, Pruning\Models\PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count());
197223
}
198224

199225
public function testTheCommandDispatchesEvents()
@@ -202,19 +228,19 @@ public function testTheCommandDispatchesEvents()
202228

203229
$dispatcher->shouldReceive('dispatch')->once()->withArgs(function ($event) {
204230
return get_class($event) === ModelPruningStarting::class &&
205-
$event->models === [PrunableTestModelWithPrunableRecords::class];
231+
$event->models === [Pruning\Models\PrunableTestModelWithPrunableRecords::class];
206232
});
207233
$dispatcher->shouldReceive('listen')->once()->with(ModelsPruned::class, m::type(Closure::class));
208234
$dispatcher->shouldReceive('dispatch')->twice()->with(m::type(ModelsPruned::class));
209235
$dispatcher->shouldReceive('dispatch')->once()->withArgs(function ($event) {
210236
return get_class($event) === ModelPruningFinished::class &&
211-
$event->models === [PrunableTestModelWithPrunableRecords::class];
237+
$event->models === [Pruning\Models\PrunableTestModelWithPrunableRecords::class];
212238
});
213239
$dispatcher->shouldReceive('forget')->once()->with(ModelsPruned::class);
214240

215241
Application::getInstance()->instance(DispatcherContract::class, $dispatcher);
216242

217-
$this->artisan(['--model' => PrunableTestModelWithPrunableRecords::class]);
243+
$this->artisan(['--model' => Pruning\Models\PrunableTestModelWithPrunableRecords::class]);
218244
}
219245

220246
protected function artisan($arguments)
@@ -238,57 +264,3 @@ protected function tearDown(): void
238264
m::close();
239265
}
240266
}
241-
242-
class PrunableTestModelWithPrunableRecords extends Model
243-
{
244-
use MassPrunable;
245-
246-
protected $table = 'prunables';
247-
protected $connection = 'default';
248-
249-
public function pruneAll()
250-
{
251-
event(new ModelsPruned(static::class, 10));
252-
event(new ModelsPruned(static::class, 20));
253-
254-
return 20;
255-
}
256-
257-
public function prunable()
258-
{
259-
return static::where('value', '>=', 3);
260-
}
261-
}
262-
263-
class PrunableTestSoftDeletedModelWithPrunableRecords extends Model
264-
{
265-
use MassPrunable, SoftDeletes;
266-
267-
protected $table = 'prunables';
268-
protected $connection = 'default';
269-
270-
public function prunable()
271-
{
272-
return static::where('value', '>=', 3);
273-
}
274-
}
275-
276-
class PrunableTestModelWithoutPrunableRecords extends Model
277-
{
278-
use Prunable;
279-
280-
public function pruneAll()
281-
{
282-
return 0;
283-
}
284-
}
285-
286-
class NonPrunableTestModel extends Model
287-
{
288-
// ..
289-
}
290-
291-
trait NonPrunableTrait
292-
{
293-
use Prunable;
294-
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Illuminate\Tests\Database\Pruning\Models;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Prunable;
9+
10+
abstract class AbstractPrunableModel extends Model
11+
{
12+
use Prunable;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Illuminate\Tests\Database\Pruning\Models;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class NonPrunableTestModel extends Model
10+
{
11+
// ..
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Illuminate\Tests\Database\Pruning\Models;
6+
7+
use Illuminate\Database\Eloquent\Prunable;
8+
9+
trait NonPrunableTrait
10+
{
11+
use Prunable;
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Illuminate\Tests\Database\Pruning\Models;
6+
7+
use Illuminate\Database\Eloquent\MassPrunable;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Events\ModelsPruned;
10+
11+
class PrunableTestModelWithPrunableRecords extends Model
12+
{
13+
use MassPrunable;
14+
15+
protected $table = 'prunables';
16+
protected $connection = 'default';
17+
18+
public function pruneAll()
19+
{
20+
event(new ModelsPruned(static::class, 10));
21+
event(new ModelsPruned(static::class, 20));
22+
23+
return 20;
24+
}
25+
26+
public function prunable()
27+
{
28+
return static::where('value', '>=', 3);
29+
}
30+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Illuminate\Tests\Database\Pruning\Models;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Prunable;
9+
10+
class PrunableTestModelWithoutPrunableRecords extends Model
11+
{
12+
use Prunable;
13+
14+
public function pruneAll()
15+
{
16+
return 0;
17+
}
18+
}

0 commit comments

Comments
 (0)