XML dla struktury: SKU, EAN, Nazwa, ..., Opis, Opis dodatkowy, Photo1..Photo6 * - pomija wiersze bez SKU * - Photo1..Photo6 łączy w jedno pole rozdzielone przecinkami * - Opis + Opis dodatkowy łączy w jedno pole (CDATA) * HTTP: csv2xml_sku_one_photos_field.php?url=URL_CSV[&filename=produkty_2.xml][&download=1] * CLI: php csv2xml_sku_one_photos_field.php URL_CSV [produkty_2.xml] */ declare(strict_types=1); $isCli = (php_sapi_name() === 'cli'); if ($isCli) { if ($argc < 2) { fwrite(STDERR, "Użycie: php {$argv[0]} URL [plik_wyjściowy.xml]\n"); exit(1); } $csvUrl = $argv[1]; $outName = $argv[2] ?? 'produkty_2.xml'; $download = false; } else { if (!isset($_GET['url']) || !filter_var($_GET['url'], FILTER_VALIDATE_URL)) { http_response_code(400); echo "Parametr ?url= jest wymagany i musi być poprawnym URL-em."; exit; } $csvUrl = $_GET['url']; $outName = (isset($_GET['filename']) && preg_match('/^[\w\-.]+$/', $_GET['filename'])) ? $_GET['filename'] : 'produkty_2.xml'; $download = isset($_GET['download']) && $_GET['download'] == '1'; } // 1) Pobierz CSV i zapisz do pliku tymczasowego (ochrona pól wielowierszowych) $csvRaw = @file_get_contents($csvUrl); if ($csvRaw === false) { $err = error_get_last(); $msg = $err['message'] ?? 'Nieznany błąd pobierania CSV.'; if ($isCli) fwrite(STDERR, "Błąd pobierania CSV: $msg\n"); else { http_response_code(502); echo "Błąd pobierania CSV: $msg"; } exit(1); } $csvRaw = preg_replace('/^\xEF\xBB\xBF/', '', $csvRaw); if (!mb_check_encoding($csvRaw, 'UTF-8')) { $csvRaw = @iconv('WINDOWS-1250', 'UTF-8//TRANSLIT', $csvRaw) ?: $csvRaw; } $tmpFile = tempnam(sys_get_temp_dir(), 'csv2xml_'); file_put_contents($tmpFile, $csvRaw); // 2) Wczytaj CSV przez fgetcsv $fh = fopen($tmpFile, 'r'); if ($fh === false) { if ($isCli) fwrite(STDERR, "Nie można otworzyć pliku tymczasowego.\n"); else { http_response_code(500); echo "Nie można otworzyć pliku tymczasowego."; } @unlink($tmpFile); exit(1); } $rows = []; while (($row = fgetcsv($fh, 0, ';', '"', '\\')) !== false) { if (count($row) === 1 && trim((string)$row[0]) === '') continue; $rows[] = $row; } fclose($fh); @unlink($tmpFile); if (count($rows) < 2) { $msg = "CSV nie zawiera danych (brakuje nagłówków lub wierszy)."; if ($isCli) fwrite(STDERR, $msg . PHP_EOL); else { http_response_code(422); echo $msg; } exit(1); } // 3) Nagłówki i mapowanie tagów $headers = array_map(fn($h) => trim((string)$h, " \t\n\r\0\x0B\""), $rows[0]); // usuń też otaczające cudzysłowy $dataRows = array_slice($rows, 1); function normalize_tag_name(string $name): string { $map = ['ą'=>'a','ć'=>'c','ę'=>'e','ł'=>'l','ń'=>'n','ó'=>'o','ś'=>'s','ż'=>'z','ź'=>'z', 'Ą'=>'A','Ć'=>'C','Ę'=>'E','Ł'=>'L','Ń'=>'N','Ó'=>'O','Ś'=>'S','Ż'=>'Z','Ź'=>'Z']; $name = strtr($name, $map); $name = preg_replace('/[^\p{L}\p{N}\s_-]+/u', ' ', $name); $name = preg_replace('/\s+/', '_', $name); $name = preg_replace('/_+/', '_', $name); $name = trim($name, '_'); if ($name === '') $name = 'field'; if (preg_match('/^\d/', $name)) $name = 'f_' . $name; return $name; } $tagMap = []; foreach ($headers as $h) $tagMap[$h] = normalize_tag_name($h); // Indeksy: SKU, Photo*, Opis, Opis dodatkowy $skuIdx = null; $photoIdxs = []; $opisIdx = null; $opisDodIdx = null; foreach ($headers as $i => $h) { $H = mb_strtoupper($h, 'UTF-8'); if ($H === 'SKU') $skuIdx = $i; if (preg_match('/^PHOTO\d+$/i', $h)) $photoIdxs[] = $i; if ($H === 'OPIS') $opisIdx = $i; if ($H === 'OPIS DODATKOWY') $opisDodIdx = $i; } if ($skuIdx === null) { $msg = "Nie znaleziono kolumny 'SKU' w nagłówkach CSV."; if ($isCli) { fwrite(STDERR, $msg . PHP_EOL); exit(1); } http_response_code(422); echo $msg; exit; } // 4) Helpery function looks_like_html_or_multiline(string $v): bool { if (strpos($v, "\n") !== false || strpos($v, "\r") !== false) return true; return (bool) preg_match('/<[^>]+>|\&(?:nbsp|lt|gt|amp|quot|#\d+);/i', $v); } function addChildWithCDATA(SimpleXMLElement $parent, string $name, string $val): SimpleXMLElement { $child = $parent->addChild($name); $node = dom_import_simplexml($child); if ($node) $node->appendChild($node->ownerDocument->createCDATASection($val)); return $child; } // 5) Budowa XML (pomijamy wiersze bez SKU; Photos jako CSV w jednym polu; Opis łączony) $xml = new SimpleXMLElement(''); $exported = 0; $skippedNoSku = 0; foreach ($dataRows as $row) { if (count($row) < count($headers)) $row = array_pad($row, count($headers), ''); // sprawdź SKU $skuRaw = isset($row[$skuIdx]) ? (string)$row[$skuIdx] : ''; $skuClean = preg_replace('/\s+/', '', trim($skuRaw)); if ($skuClean === '' || $skuClean === null) { $skippedNoSku++; continue; } $product = $xml->addChild('Product'); // Zbierz zdjęcia $photos = []; foreach ($photoIdxs as $pi) { $v = isset($row[$pi]) ? trim((string)$row[$pi]) : ''; if ($v !== '') $photos[] = $v; } // Zbierz i połącz Opis + Opis dodatkowy $opisVal = ''; $o1 = ($opisIdx !== null) ? trim((string)$row[$opisIdx]) : ''; $o2 = ($opisDodIdx !== null) ? trim((string)$row[$opisDodIdx]) : ''; if ($o1 !== '' && $o2 !== '') $opisVal = $o1 . "\n\n" . $o2; elseif ($o1 !== '') $opisVal = $o1; elseif ($o2 !== '') $opisVal = $o2; foreach ($headers as $i => $header) { // Pomiń kolumny Photo* oraz oryginalne kolumny Opis i Opis dodatkowy (dodamy jeden wspólny niżej) if (in_array($i, $photoIdxs, true)) continue; $H = mb_strtoupper($header, 'UTF-8'); if ($H === 'OPIS' || $H === 'OPIS DODATKOWY') continue; $rawVal = isset($row[$i]) ? (string)$row[$i] : ''; $val = trim($rawVal, " \t\n\r\0\x0B\""); $tag = $tagMap[$header] ?? ('field_' . $i); if ($val === '') { $product->addChild($tag, ''); continue; } // Opis/HTML → CDATA (dla zwykłych pól) if (looks_like_html_or_multiline($val) || preg_match('/\bopis\b/i', $header)) { addChildWithCDATA($product, $tag, $val); } else { if (preg_match('/^(EAN|SKU)$/ui', $header)) $val = preg_replace('/\s+/', '', $val); $product->addChild($tag, $val); } } // Dodaj jedno pole Photos rozdzielone przecinkami $product->addChild('Photos', !empty($photos) ? implode(',', $photos) : ''); // Dodaj połączony Opis jako jedno pole (CDATA) // Użyj istniejącej zmapowanej nazwy "Opis" jeśli była w nagłówku, w przeciwnym wypadku "Opis" $opisTag = $tagMap['Opis'] ?? 'Opis'; addChildWithCDATA($product, $opisTag, $opisVal); $exported++; } // 6) Formatowanie i zapis $dom = new DOMDocument('1.0', 'UTF-8'); $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadXML($xml->asXML()); if ($isCli) { if (@$dom->save($outName) === false) { fwrite(STDERR, "Nie udało się zapisać do pliku: {$outName}\n"); exit(1); } echo "Zapisano XML do: {$outName}\n"; echo "Wyeksportowano: {$exported}, pominięto (brak SKU): {$skippedNoSku}\n"; exit(0); } // HTTP zapis $saveDir = __DIR__ . DIRECTORY_SEPARATOR . 'exports'; if (!is_dir($saveDir)) { if (!@mkdir($saveDir, 0775, true) && !is_dir($saveDir)) { http_response_code(500); echo "Nie można utworzyć katalogu zapisu: {$saveDir}"; exit; } } $outName = preg_match('/^[\w\-.]+$/', $outName) ? $outName : ('produkty_' . date('Ymd_His') . '.xml'); $savePath = $saveDir . DIRECTORY_SEPARATOR . $outName; if (@$dom->save($savePath) === false) { http_response_code(500); echo "Nie udało się zapisać pliku: {$savePath}"; exit; } $basePath = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/'); $publicRel = $basePath . '/exports/' . rawurlencode($outName); $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? ''; $publicUrl = $host ? ($scheme . '://' . $host . $publicRel) : $publicRel; if ($download) { header('Content-Type: application/xml; charset=UTF-8'); header('Content-Disposition: attachment; filename="'.$outName.'"'); readfile($savePath); exit; } header('Content-Type: application/json; charset=UTF-8'); echo json_encode([ 'status' => 'ok', 'message' => 'Plik XML zapisany na serwerze.', 'file_path' => $savePath, 'file_url' => $publicUrl, 'filename' => $outName, 'exported' => $exported, 'skipped_no_sku' => $skippedNoSku, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);