The Prunable trait declares which rows are expendable and does nothing until something runs the command.
final class ReceivedWebhook extends Model
{
use Prunable;
public function prunable(): Builder
{
return static::where('created_at', '<', now()->subDays(14));
}
protected function pruning(): void
{
Storage::delete($this->payload_path); // per row
}
}
// and the half that is always missing:
// $schedule->command('model:prune')->daily();
The pruning hook loads and deletes row by row, which is correct when there is an associated file and is a very slow way to delete four million rows. MassPrunable is the alternative and skips the hook entirely, so the choice is between per-row cleanup and a bulk delete — and choosing wrongly means either a job that runs for hours or orphaned files nobody notices.