Files
SpiceManagementSystem/app/Console/Commands/ShopifyImport.php
T
Malek TellissiandClaude Sonnet 4.6 9ee67dfb69
Deploy / deploy (push) Successful in 13s
Download and store Shopify product images locally on import
Images are fetched from Shopify CDN, stored in storage/app/public/product-images/
as UUIDs, and saved in the Product.images array — matching Filament's FileUpload format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-11 15:08:01 +02:00

159 lines
5.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Console\Commands;
use App\Models\Packing;
use App\Models\Product;
use App\Models\ProductInventory;
use App\Models\ProductSize;
use App\Services\ShopifyService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ShopifyImport extends Command
{
protected $signature = 'shopify:import {--dry-run : Nur anzeigen, nichts speichern}';
protected $description = 'Importiert Produkte und Lagerbestand direkt aus der Shopify API';
public function handle(ShopifyService $shopify): int
{
$dryRun = $this->option('dry-run');
$variants = $shopify->getAllVariants();
if (empty($variants)) {
$this->error('Keine Varianten von Shopify erhalten (Details im Log).');
return self::FAILURE;
}
$this->info('Shopify-Varianten geladen: ' . count($variants));
$stats = ['products' => 0, 'sizes' => 0, 'inventory' => 0, 'skipped' => 0];
DB::transaction(function () use ($variants, $dryRun, &$stats) {
foreach ($variants as $variant) {
$packingRaw = $variant['option1'];
$parsed = $this->parsePacking($packingRaw, $variant['sku']);
if ($parsed === null) {
$this->warn(" ⚠ Verpackungsformat nicht erkannt: '{$packingRaw}' ({$variant['sku']}) – übersprungen");
$stats['skipped']++;
continue;
}
if ($dryRun) {
$this->line(" {$variant['product_title']} | {$parsed['name']} {$parsed['size']} | SKU {$variant['sku']} | Bestand {$variant['inventory_quantity']}");
continue;
}
$packing = Packing::firstOrCreate([
'name' => $parsed['name'],
'size' => $parsed['size'],
]);
$product = Product::firstOrCreate(
['title' => $variant['product_title']],
['recipes_id' => null],
);
if ($product->wasRecentlyCreated) {
$stats['products']++;
$this->downloadImages($product, $variant['image_urls']);
}
$size = ProductSize::firstOrCreate(
['product_id' => $product->id, 'packing_id' => $packing->id],
['sku' => $variant['sku'], 'shopify_variant_id' => $variant['id'], 'price' => 0],
);
// Bestehende Size ohne Variant-ID nachträglich verknüpfen
if (! $size->wasRecentlyCreated && ! $size->shopify_variant_id) {
$size->update(['shopify_variant_id' => $variant['id'], 'sku' => $variant['sku']]);
}
if ($size->wasRecentlyCreated) {
$stats['sizes']++;
}
// Lagerbestand: aktuelle Differenz auffüllen
$localQty = ProductInventory::where('size_id', $size->id)->whereNull('out_at')->count();
$diff = $variant['inventory_quantity'] - $localQty;
if ($diff > 0) {
for ($i = 0; $i < $diff; $i++) {
ProductInventory::create([
'product_id' => $product->id,
'size_id' => $size->id,
]);
}
$stats['inventory'] += $diff;
}
}
});
if (! $dryRun) {
$this->info("✓ {$stats['products']} Produkte erstellt");
$this->info("✓ {$stats['sizes']} Varianten erstellt");
$this->info("✓ {$stats['inventory']} Lagereinträge erstellt");
}
if ($stats['skipped']) {
$this->warn("⚠ {$stats['skipped']} Varianten übersprungen (Verpackungsformat unbekannt)");
}
return self::SUCCESS;
}
private function downloadImages(Product $product, array $urls): void
{
if (empty($urls) || ! empty($product->images)) {
return;
}
$paths = [];
foreach ($urls as $url) {
$response = Http::get($url);
if ($response->failed()) {
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());
$paths[] = $filename;
}
if (! empty($paths)) {
$product->update(['images' => $paths]);
}
}
private function parsePacking(string $option, string $sku): ?array
{
// "Standbodenbeutel 100 g" / "Metalldose 40 g"
if (preg_match('/^(.+?)\s+(\d+(?:[.,]\d+)?)\s*g$/i', $option, $m)) {
return ['name' => trim($m[1]), 'size' => (float) str_replace(',', '.', $m[2])];
}
// "Metalldose 5 Stück" / "Standbodenbeutel 3 Stück"
if (preg_match('/^(.+?)\s+(\d+)\s*Stück$/i', $option, $m)) {
return ['name' => trim($m[1]) . ' (Stück)', 'size' => (float) $m[2]];
}
// "1 Stange" / "3 Stangen" / "5 Stangen"
if (preg_match('/^(\d+)\s*Stangen?$/i', $option, $m)) {
return ['name' => 'Stangen', 'size' => (float) $m[1]];
}
// "Default Title" — Verpackung aus SKU ableiten: z.B. GWM-AAS-100SB → 100g Standbodenbeutel
if ($option === 'Default Title' && preg_match('/(\d+)(SB|MD|GT|BB)$/i', $sku, $m)) {
$names = ['SB' => 'Standbodenbeutel', 'MD' => 'Metalldose', 'GT' => 'Glas', 'BB' => 'Beutel'];
return ['name' => $names[strtoupper($m[2])], 'size' => (float) $m[1]];
}
return null;
}
}