<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\MassPrunable;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Events\ModelsPruned;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Finder\Finder;
class PruneCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'model:prune
{--model=* : Class names of the models to be pruned}
{--except=* : Class names of the models to be excluded from pruning}
{--chunk=1000 : The number of models to retrieve per chunk of models to be deleted}
{--pretend : Display the number of prunable records found instead of deleting them}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune models that are no longer needed';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function handle(Dispatcher $events)
{
$models = $this->models();
if ($models->isEmpty()) {
$this->components->info('No prunable models found.');
return;
}
if ($this->option('pretend')) {
$models->each(function ($model) {
$this->pretendToPrune($model);
});
return;
}
$pruning = [];
$events->listen(ModelsPruned::class, function ($event) use (&$pruning) {
if (! in_array($event->model, $pruning)) {
$pruning[] = $event->model;
$this->newLine();
$this->components->info(sprintf('Pruning [%s] records.', $event->model));
}
$this->components->twoColumnDetail($event->model, "{$event->count} records");
});
$models->each(function ($model) {
$this->pruneModel($model);
});
$events->forget(ModelsPruned::class);
}
/**
* Prune the given model.
*
* @param string $model
* @return void
*/
protected function pruneModel(string $model)
{
$instance = new $model;
$chunkSize = property_exists($instance, 'prunableChunkSize')
? $instance->prunableChunkSize
: $this->option('chunk');
$total = $this->isPrunable($model)
? $instance->pruneAll($chunkSize)
: 0;
if ($total == 0) {
$this->components->info("No prunable [$model] records found.");
}
}
/**
* Determine the models that should be pruned.
*
* @return \Illuminate\Support\Collection
*/
protected function models()
{
if (! empty($models = $this->option('model'))) {
return collect($models)->filter(function ($model) {
return class_exists($model);
})->values();
}
$except = $this->option('except');
if (! empty($models) && ! empty($except)) {
throw new InvalidArgumentException('The --models and --except options cannot be combined.');
}
return collect((new Finder)->in($this->getDefaultPath())->files()->name('*.php'))
->map(function ($model) {
$namespace = $this->laravel->getNamespace();
return $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
);
})->when(! empty($except), function ($models) use ($except) {
return $models->reject(function ($model) use ($except) {
return in_array($model, $except);
});
})->filter(function ($model) {
return $this->isPrunable($model);
})->filter(function ($model) {
return class_exists($model);
})->values();
}
/**
* Get the default path where models are located.
*
* @return string|string[]
*/
protected function getDefaultPath()
{
return app_path('Models');
}
/**
* Determine if the given model class is prunable.
*
* @param string $model
* @return bool
*/
protected function isPrunable($model)
{
$uses = class_uses_recursive($model);
return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses);
}
/**
* Display how many models will be pruned.
*
* @param string $model
* @return void
*/
protected function pretendToPrune($model)
{
$instance = new $model;
$count = $instance->prunable()
->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($instance))), function ($query) {
$query->withTrashed();
})->count();
if ($count === 0) {
$this->components->info("No prunable [$model] records found.");
} else {
$this->components->info("{$count} [{$model}] records will be pruned.");
}
}
}