From 4e15d8ef8cef52905e961588c3837f50d9267b05 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Tue, 22 Sep 2026 21:17:35 +0300 Subject: [PATCH] Fix: Updating Wipe Catalog Command to force delete products instead of the soft delete --- src/Command/WipeCatalogCommand.php | 34 ++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Command/WipeCatalogCommand.php b/src/Command/WipeCatalogCommand.php index b7bc8cc..22f237f 100644 --- a/src/Command/WipeCatalogCommand.php +++ b/src/Command/WipeCatalogCommand.php @@ -50,7 +50,12 @@ class WipeCatalogCommand extends Command 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) { $this->info('No products exist — nothing to do.'); @@ -146,7 +151,16 @@ class WipeCatalogCommand extends Command ImportMapping::whereIn('source_type', ['product', 'variant', 'image'])->delete(); 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) ->get(); @@ -161,10 +175,22 @@ class WipeCatalogCommand extends Command foreach ($product->variants as $variant) { $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(); } }