Feat: Updating ShopifyExportImporter and WipeCatalogCommand to handle images

This commit is contained in:
2026-09-22 15:29:03 +03:00
parent 37b49963f6
commit 59c57b37fc
2 changed files with 53 additions and 18 deletions
+36 -17
View File
@@ -124,30 +124,49 @@ class WipeCatalogCommand extends Command
* same way regardless. media_variant and product_option_value_ * same way regardless. media_variant and product_option_value_
* product_variant DO cascade at the DB level (see their own * product_variant DO cascade at the DB level (see their own
* migrations), so deleting the variant itself is enough for those two. * migrations), so deleting the variant itself is enough for those two.
*
* Deliberately NOT chunkById() — that re-queries "id > lastSeenId"
* every iteration, but deleting rows inside the loop shrinks the
* table out from under it: any product whose id fell in a range
* chunkById() had already stepped past could be silently skipped and
* never actually deleted at all. Caught in practice — the first real
* run of this command left orphaned Media rows (Spatie's own
* deleteAllMedia(), fired from Product's `deleting` event, never ran
* for the skipped products) whose 'image' ImportMapping rows then
* caused a LATER Shopify re-import to silently reuse those now-
* orphaned Media objects instead of importing fresh ones — see
* MigrateImport\Shopify\ShopifyExportImporter::resolveOrImportImage()'s
* own docblock for that half of the same incident. Always re-querying
* the first N remaining rows (never advancing an id cursor) guarantees
* every product is actually visited exactly once, however many are
* deleted out from under the query as it goes.
*/ */
private function wipe(): void private function wipe(): void
{ {
ImportMapping::where('source_type', 'product')->delete(); ImportMapping::whereIn('source_type', ['product', 'variant', 'image'])->delete();
ImportMapping::where('source_type', 'variant')->delete();
// Model-by-model, not a bulk query — see class docblock on why while (true) {
// this must go through Eloquent for Spatie's media cleanup to $products = Product::with(['variants', 'associations', 'inverseAssociations'])
// fire on both Product and ProductVariant. ->limit(100)
Product::with(['variants', 'associations', 'inverseAssociations']) ->get();
->chunkById(100, function ($products) {
foreach ($products as $product) {
$product->associations()->delete();
$product->inverseAssociations()->delete();
$product->productOptions()->detach();
foreach ($product->variants as $variant) { if ($products->isEmpty()) {
$variant->prices()->delete(); break;
$variant->delete(); }
}
$product->delete(); foreach ($products as $product) {
$product->associations()->delete();
$product->inverseAssociations()->delete();
$product->productOptions()->detach();
foreach ($product->variants as $variant) {
$variant->prices()->delete();
$variant->delete();
} }
});
$product->delete();
}
}
Product::removeAllFromSearch(); Product::removeAllFromSearch();
} }
@@ -245,7 +245,23 @@ class ShopifyExportImporter implements Importer
$existing = ImportMapping::resolve(self::SOURCE, 'image', $externalId); $existing = ImportMapping::resolve(self::SOURCE, 'image', $externalId);
if ($existing instanceof Media) { // ImportMapping is a durable record of "we already imported this,"
// but the Media row it points at can go stale — e.g. Command\
// WipeCatalogCommand deletes every Product (media included, via
// Spatie's own model-delete cleanup) without knowing this mapping
// exists, since the mapping ISN'T scoped to a single Product to
// clean up alongside it. Re-running an import afterward used to
// trust the cached Media object unconditionally — it still existed
// as a PHP object even though its underlying row (and file) were
// long gone, so every re-imported product silently got zero
// media, no error, no warning. Falls through to a fresh import
// whenever the mapping doesn't resolve to a real, still-attached
// Media row.
if ($existing instanceof Media
&& Media::whereKey($existing->getKey())->exists()
&& $existing->model_type === $product->getMorphClass()
&& (int) $existing->model_id === $product->id
) {
return $existing; return $existing;
} }