From 822790bfc0e649872800395f23b9f64e651faffa Mon Sep 17 00:00:00 2001 From: Malek Tellissi Date: Sat, 12 Sep 2026 04:16:12 +0200 Subject: [PATCH] Improve image import logging and move HTTP downloads out of DB transaction Co-Authored-By: Claude Sonnet 4.6 --- app/Console/Commands/ShopifyImages.php | 39 ++++++++++++++++--- app/Console/Commands/ShopifyImport.php | 33 ++++++++++++++-- .../User/Resources/ProductResource.php | 1 + 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/app/Console/Commands/ShopifyImages.php b/app/Console/Commands/ShopifyImages.php index bcfb447..0a201f2 100644 --- a/app/Console/Commands/ShopifyImages.php +++ b/app/Console/Commands/ShopifyImages.php @@ -24,19 +24,29 @@ class ShopifyImages extends Command } // Deduplizieren: pro Produkttitel nur einmal - $products = collect($variants) + $shopifyProducts = collect($variants) ->filter(fn($v) => ! empty($v['image_urls'])) ->keyBy('product_title'); - $this->info("Produkte mit Bildern in Shopify: {$products->count()}"); + $totalInShopify = collect($variants)->pluck('product_title')->unique()->count(); + $this->info("Shopify-Produkte gesamt: {$totalInShopify}"); + $this->info("Davon mit Bild-URLs: {$shopifyProducts->count()}"); + + if ($shopifyProducts->isEmpty()) { + $this->warn('Kein Shopify-Produkt hat Bild-URLs — Import nicht möglich.'); + return self::SUCCESS; + } $downloaded = 0; $skipped = 0; + $notFound = 0; - foreach ($products as $title => $variant) { + foreach ($shopifyProducts as $title => $variant) { $product = Product::where('title', $title)->first(); if (! $product) { + $this->warn(" Nicht lokal gefunden: \"{$title}\""); + $notFound++; continue; } @@ -47,15 +57,27 @@ class ShopifyImages extends Command $paths = []; foreach ($variant['image_urls'] as $url) { - $response = Http::get($url); + try { + $response = Http::timeout(30)->get($url); + } catch (\Exception $e) { + $this->warn(" Verbindungsfehler für {$url}: {$e->getMessage()}"); + continue; + } + if ($response->failed()) { - $this->warn(" Bild konnte nicht geladen werden: {$url}"); + $this->warn(" HTTP {$response->status()} für: {$url}"); + continue; + } + + $body = $response->body(); + if (empty($body)) { + $this->warn(" Leere Antwort für: {$url}"); continue; } $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'jpg'; $filename = 'product-images/' . Str::uuid() . '.' . $ext; - Storage::disk('public')->put($filename, $response->body()); + Storage::disk('public')->put($filename, $body); $paths[] = $filename; } @@ -63,6 +85,8 @@ class ShopifyImages extends Command $product->update(['images' => $paths]); $this->line(" ✓ {$title} (" . count($paths) . " Bild(er))"); $downloaded++; + } else { + $this->warn(" Keine Bilder gespeichert für: \"{$title}\""); } } @@ -70,6 +94,9 @@ class ShopifyImages extends Command if ($skipped) { $this->line(" {$skipped} übersprungen (haben bereits Bilder — nutze --force zum Überschreiben)"); } + if ($notFound) { + $this->warn(" {$notFound} Shopify-Produkte lokal nicht gefunden (Titelabweichung?)"); + } return self::SUCCESS; } diff --git a/app/Console/Commands/ShopifyImport.php b/app/Console/Commands/ShopifyImport.php index 45d510e..44fba00 100644 --- a/app/Console/Commands/ShopifyImport.php +++ b/app/Console/Commands/ShopifyImport.php @@ -32,7 +32,9 @@ class ShopifyImport extends Command $stats = ['products' => 0, 'sizes' => 0, 'inventory' => 0, 'skipped' => 0]; - DB::transaction(function () use ($variants, $dryRun, &$stats) { + $newProducts = []; + + DB::transaction(function () use ($variants, $dryRun, &$stats, &$newProducts) { foreach ($variants as $variant) { $packingRaw = $variant['option1']; @@ -60,7 +62,11 @@ class ShopifyImport extends Command ); if ($product->wasRecentlyCreated) { $stats['products']++; - $this->downloadImages($product, $variant['image_urls']); + // Collect for image download after transaction + $newProducts[$product->id] = [ + 'product' => $product, + 'image_urls' => $variant['image_urls'], + ]; } $size = ProductSize::firstOrCreate( @@ -93,6 +99,11 @@ class ShopifyImport extends Command } }); + // Download images outside the transaction to avoid holding DB locks during HTTP calls + foreach ($newProducts as ['product' => $product, 'image_urls' => $urls]) { + $this->downloadImages($product, $urls); + } + if (! $dryRun) { $this->info("✓ {$stats['products']} Produkte erstellt"); $this->info("✓ {$stats['sizes']} Varianten erstellt"); @@ -114,19 +125,33 @@ class ShopifyImport extends Command $paths = []; foreach ($urls as $url) { - $response = Http::get($url); + try { + $response = Http::timeout(30)->get($url); + } catch (\Exception $e) { + $this->warn(" Bild-Download fehlgeschlagen ({$product->title}): {$e->getMessage()}"); + continue; + } + if ($response->failed()) { + $this->warn(" HTTP {$response->status()} beim Bild-Download ({$product->title}): {$url}"); + continue; + } + + $body = $response->body(); + if (empty($body)) { + $this->warn(" Leere Bild-Antwort ({$product->title}): {$url}"); continue; } $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'jpg'; $filename = 'product-images/' . Str::uuid() . '.' . $ext; - Storage::disk('public')->put($filename, $response->body()); + Storage::disk('public')->put($filename, $body); $paths[] = $filename; } if (! empty($paths)) { $product->update(['images' => $paths]); + $this->line(" ✓ Bilder gespeichert: {$product->title} (" . count($paths) . ')'); } } diff --git a/app/Filament/User/Resources/ProductResource.php b/app/Filament/User/Resources/ProductResource.php index 89a5cb6..ba7de1e 100644 --- a/app/Filament/User/Resources/ProductResource.php +++ b/app/Filament/User/Resources/ProductResource.php @@ -100,6 +100,7 @@ class ProductResource extends Resource { ->columns([ Tables\Columns\ImageColumn::make('images') ->label('Bild') + ->disk('public') ->limit(1), Tables\Columns\TextColumn::make('title') ->label('Name')