baseUrl = "https://{$domain}/admin/api/{$version}"; $this->token = config('services.shopify.access_token'); } /** * Returns all product variants from Shopify. * Each entry: ['id', 'sku', 'inventory_quantity', 'product_title'] */ public function getAllVariants(): array { $variants = []; $url = "{$this->baseUrl}/products.json?limit=250&fields=id,title,variants"; while ($url) { $response = Http::withHeader('X-Shopify-Access-Token', $this->token)->get($url); if ($response->failed()) { $status = $response->status(); $hint = match (true) { $status === 401 => 'Access Token ungültig oder fehlt', $status === 403 => 'IP-Whitelist: VPS-IP ist in Shopify nicht freigeschalten', $status === 429 => 'Rate Limit überschritten', $status >= 500 => 'Shopify-seitiger Fehler', default => 'Unbekannter Fehler', }; Log::error("Shopify Sync fehlgeschlagen: {$hint}", [ 'status' => $status, 'url' => $url, 'body' => $response->body(), ]); break; } foreach ($response->json('products', []) as $product) { foreach ($product['variants'] as $variant) { $variants[] = [ 'id' => (string) $variant['id'], 'sku' => $variant['sku'] ?? '', 'inventory_quantity' => (int) ($variant['inventory_quantity'] ?? 0), 'product_title' => $product['title'], ]; } } // Follow Shopify's Link header pagination $url = $this->nextPageUrl($response->header('Link')); if ($url) { usleep(500_000); // 0.5s — stay well within 2 req/s limit } } return $variants; } private function nextPageUrl(?string $linkHeader): ?string { if (!$linkHeader) { return null; } // Link: ; rel="next" if (preg_match('/<([^>]+)>;\s*rel="next"/', $linkHeader, $m)) { return $m[1]; } return null; } }