Fix: Updating Wipe Catalog Command to force delete products instead of the soft delete

This commit is contained in:
2026-09-22 21:17:35 +03:00
parent 1c7efc6e4d
commit 4e15d8ef8c
+30 -4
View File
@@ -50,7 +50,12 @@ class WipeCatalogCommand extends Command
public function handle(OtpService $otp): int public function handle(OtpService $otp): int
{ {
$productCount = Product::count(); // withTrashed() — a prior soft-delete-only bug in this command
// (fixed in wipe() below) could leave ghost rows a plain count()
// would never see, silently reporting "nothing to do" while they
// sit there breaking other things (e.g. the admin's own global
// search, which assumes every returned product has variants).
$productCount = Product::withTrashed()->count();
if ($productCount === 0) { if ($productCount === 0) {
$this->info('No products exist — nothing to do.'); $this->info('No products exist — nothing to do.');
@@ -146,7 +151,16 @@ class WipeCatalogCommand extends Command
ImportMapping::whereIn('source_type', ['product', 'variant', 'image'])->delete(); ImportMapping::whereIn('source_type', ['product', 'variant', 'image'])->delete();
while (true) { while (true) {
$products = Product::with(['variants', 'associations', 'inverseAssociations']) // withTrashed(): Product/ProductVariant both use SoftDeletes
// — a plain query would stop seeing a product the moment
// forceDelete() below actually removes it, which is fine, but
// WITHOUT withTrashed() here this loop would never even
// fetch a row that a previous, buggy run of this command
// (or any other code) had already soft-deleted without
// force-deleting it. Ghost rows like that are exactly what
// this command exists to remove.
$products = Product::withTrashed()
->with(['variants' => fn ($query) => $query->withTrashed(), 'associations', 'inverseAssociations'])
->limit(100) ->limit(100)
->get(); ->get();
@@ -161,10 +175,22 @@ class WipeCatalogCommand extends Command
foreach ($product->variants as $variant) { foreach ($product->variants as $variant) {
$variant->prices()->delete(); $variant->prices()->delete();
$variant->delete(); // NOT delete() — Product/ProductVariant both use
// SoftDeletes, and a plain delete() only sets
// deleted_at, leaving the row (and, for Product, its
// media) sitting in the table. This command's whole
// purpose is an irreversible wipe; a soft-deleted
// ghost row is the opposite of that. Caught in
// practice — a prior run's plain delete() left 185
// ghost Product rows with zero real variants, which
// then crashed the admin's own global search
// (Lunar\Admin\Filament\Resources\ProductResource::
// getGlobalSearchResultDetails() assumes
// $record->variants->first() is never null).
$variant->forceDelete();
} }
$product->delete(); $product->forceDelete();
} }
} }