PruneCommand.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. <?php
  2. namespace Illuminate\Database\Console;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Contracts\Events\Dispatcher;
  5. use Illuminate\Database\Eloquent\MassPrunable;
  6. use Illuminate\Database\Eloquent\Prunable;
  7. use Illuminate\Database\Eloquent\SoftDeletes;
  8. use Illuminate\Database\Events\ModelPruningFinished;
  9. use Illuminate\Database\Events\ModelPruningStarting;
  10. use Illuminate\Database\Events\ModelsPruned;
  11. use Illuminate\Support\Str;
  12. use InvalidArgumentException;
  13. use Symfony\Component\Console\Attribute\AsCommand;
  14. use Symfony\Component\Finder\Finder;
  15. #[AsCommand(name: 'model:prune')]
  16. class PruneCommand extends Command
  17. {
  18. /**
  19. * The console command name.
  20. *
  21. * @var string
  22. */
  23. protected $signature = 'model:prune
  24. {--model=* : Class names of the models to be pruned}
  25. {--except=* : Class names of the models to be excluded from pruning}
  26. {--path=* : Absolute path(s) to directories where models are located}
  27. {--chunk=1000 : The number of models to retrieve per chunk of models to be deleted}
  28. {--pretend : Display the number of prunable records found instead of deleting them}';
  29. /**
  30. * The console command description.
  31. *
  32. * @var string
  33. */
  34. protected $description = 'Prune models that are no longer needed';
  35. /**
  36. * Execute the console command.
  37. *
  38. * @param \Illuminate\Contracts\Events\Dispatcher $events
  39. * @return void
  40. */
  41. public function handle(Dispatcher $events)
  42. {
  43. $models = $this->models();
  44. if ($models->isEmpty()) {
  45. $this->components->info('No prunable models found.');
  46. return;
  47. }
  48. if ($this->option('pretend')) {
  49. $models->each(function ($model) {
  50. $this->pretendToPrune($model);
  51. });
  52. return;
  53. }
  54. $pruning = [];
  55. $events->listen(ModelsPruned::class, function ($event) use (&$pruning) {
  56. if (! in_array($event->model, $pruning)) {
  57. $pruning[] = $event->model;
  58. $this->newLine();
  59. $this->components->info(sprintf('Pruning [%s] records.', $event->model));
  60. }
  61. $this->components->twoColumnDetail($event->model, "{$event->count} records");
  62. });
  63. $events->dispatch(new ModelPruningStarting($models->all()));
  64. $models->each(function ($model) {
  65. $this->pruneModel($model);
  66. });
  67. $events->dispatch(new ModelPruningFinished($models->all()));
  68. $events->forget(ModelsPruned::class);
  69. }
  70. /**
  71. * Prune the given model.
  72. *
  73. * @param string $model
  74. * @return void
  75. */
  76. protected function pruneModel(string $model)
  77. {
  78. $instance = new $model;
  79. $chunkSize = property_exists($instance, 'prunableChunkSize')
  80. ? $instance->prunableChunkSize
  81. : $this->option('chunk');
  82. $total = $this->isPrunable($model)
  83. ? $instance->pruneAll($chunkSize)
  84. : 0;
  85. if ($total == 0) {
  86. $this->components->info("No prunable [$model] records found.");
  87. }
  88. }
  89. /**
  90. * Determine the models that should be pruned.
  91. *
  92. * @return \Illuminate\Support\Collection
  93. */
  94. protected function models()
  95. {
  96. if (! empty($models = $this->option('model'))) {
  97. return collect($models)->filter(function ($model) {
  98. return class_exists($model);
  99. })->values();
  100. }
  101. $except = $this->option('except');
  102. if (! empty($models) && ! empty($except)) {
  103. throw new InvalidArgumentException('The --models and --except options cannot be combined.');
  104. }
  105. return collect(Finder::create()->in($this->getPath())->files()->name('*.php'))
  106. ->map(function ($model) {
  107. $namespace = $this->laravel->getNamespace();
  108. return $namespace.str_replace(
  109. ['/', '.php'],
  110. ['\\', ''],
  111. Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
  112. );
  113. })->when(! empty($except), function ($models) use ($except) {
  114. return $models->reject(function ($model) use ($except) {
  115. return in_array($model, $except);
  116. });
  117. })->filter(function ($model) {
  118. return class_exists($model);
  119. })->filter(function ($model) {
  120. return $this->isPrunable($model);
  121. })->values();
  122. }
  123. /**
  124. * Get the path where models are located.
  125. *
  126. * @return string[]|string
  127. */
  128. protected function getPath()
  129. {
  130. if (! empty($path = $this->option('path'))) {
  131. return collect($path)->map(function ($path) {
  132. return base_path($path);
  133. })->all();
  134. }
  135. return app_path('Models');
  136. }
  137. /**
  138. * Determine if the given model class is prunable.
  139. *
  140. * @param string $model
  141. * @return bool
  142. */
  143. protected function isPrunable($model)
  144. {
  145. $uses = class_uses_recursive($model);
  146. return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses);
  147. }
  148. /**
  149. * Display how many models will be pruned.
  150. *
  151. * @param string $model
  152. * @return void
  153. */
  154. protected function pretendToPrune($model)
  155. {
  156. $instance = new $model;
  157. $count = $instance->prunable()
  158. ->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($instance))), function ($query) {
  159. $query->withTrashed();
  160. })->count();
  161. if ($count === 0) {
  162. $this->components->info("No prunable [$model] records found.");
  163. } else {
  164. $this->components->info("{$count} [{$model}] records will be pruned.");
  165. }
  166. }
  167. }