diff --git a/app/Console/Commands/TestProductImageMigration.php b/app/Console/Commands/TestProductImageMigration.php new file mode 100644 index 0000000..8decd97 --- /dev/null +++ b/app/Console/Commands/TestProductImageMigration.php @@ -0,0 +1,395 @@ +migrationService = $migrationService; + } + + /** + * Execute the console command. + */ + public function handle() + { + $sku = $this->argument('sku'); + + $this->info("Testing image migration for product SKU: {$sku}"); + $this->newLine(); + + // Get connection details from the service using reflection + $reflection = new \ReflectionClass($this->migrationService); + $m1ConnectionProp = $reflection->getProperty('magento1Connection'); + $m2ConnectionProp = $reflection->getProperty('magento2Connection'); + $m1PrefixProp = $reflection->getProperty('magento1Prefix'); + $m2PrefixProp = $reflection->getProperty('magento2Prefix'); + + $m1ConnectionProp->setAccessible(true); + $m2ConnectionProp->setAccessible(true); + $m1PrefixProp->setAccessible(true); + $m2PrefixProp->setAccessible(true); + + $m1Connection = $m1ConnectionProp->getValue($this->migrationService); + $m2Connection = $m2ConnectionProp->getValue($this->migrationService); + $m1Prefix = $m1PrefixProp->getValue($this->migrationService); + $m2Prefix = $m2PrefixProp->getValue($this->migrationService); + + // Step 1: Find product in M1 + $this->info("Step 1: Finding product in Magento 1..."); + $m1Product = $this->findM1ProductBySku($sku, $m1Connection, $m1Prefix); + + if (!$m1Product) { + $this->error("Product with SKU '{$sku}' not found in Magento 1"); + return 1; + } + + $m1ProductId = $m1Product->entity_id; + $this->info("✓ Found M1 product: ID {$m1ProductId}, SKU: {$sku}"); + $this->newLine(); + + // Step 2: Check images in M1 + $this->info("Step 2: Checking images in Magento 1..."); + $m1Images = $this->getM1Images($m1ProductId, $m1Connection, $m1Prefix); + $this->info("✓ Found " . count($m1Images) . " images in M1:"); + foreach ($m1Images as $idx => $image) { + $this->line(" " . ($idx + 1) . ". {$image['file']} (value_id: {$image['value_id']})"); + } + $this->newLine(); + + // Step 3: Find product in M2 + $this->info("Step 3: Finding product in Magento 2..."); + $m2Product = $this->findM2ProductBySku($sku, $m2Connection, $m2Prefix); + + if (!$m2Product) { + $this->error("Product with SKU '{$sku}' not found in Magento 2. Please migrate the product first."); + return 1; + } + + $m2ProductId = $m2Product->entity_id; + $this->info("✓ Found M2 product: ID {$m2ProductId}, SKU: {$sku}"); + $this->newLine(); + + // Step 4: Check images in M2 before migration + $this->info("Step 4: Checking images in Magento 2 BEFORE migration..."); + $m2ImagesBefore = $this->getM2Images($m2ProductId, $m2Connection, $m2Prefix); + $this->info("✓ Found " . count($m2ImagesBefore) . " images in M2 before migration"); + if (count($m2ImagesBefore) > 0) { + foreach ($m2ImagesBefore as $idx => $image) { + $this->line(" " . ($idx + 1) . ". {$image['file']} (value_id: {$image['value_id']})"); + } + } + $this->newLine(); + + // Step 5: Migrate images + $this->info("Step 5: Migrating images..."); + try { + $m1EntityTypeId = DB::connection($m1Connection) + ->table($m1Prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + // Use reflection to call the protected method + $reflection = new \ReflectionClass($this->migrationService); + $method = $reflection->getMethod('migrateProductMediaGallery'); + $method->setAccessible(true); + + $method->invoke($this->migrationService, $m1ProductId, $m2ProductId, $m1EntityTypeId); + + $this->info("✓ Migration method executed"); + } catch (\Exception $e) { + $this->error("✗ Error during migration: " . $e->getMessage()); + $this->error("Stack trace: " . $e->getTraceAsString()); + return 1; + } + $this->newLine(); + + // Step 6: Check images in M2 after migration + $this->info("Step 6: Checking images in Magento 2 AFTER migration..."); + $m2ImagesAfter = $this->getM2Images($m2ProductId, $m2Connection, $m2Prefix); + $this->info("✓ Found " . count($m2ImagesAfter) . " images in M2 after migration"); + + if (count($m2ImagesAfter) > 0) { + foreach ($m2ImagesAfter as $idx => $image) { + $this->line(" " . ($idx + 1) . ". {$image['file']} (value_id: {$image['value_id']})"); + } + } else { + $this->error("✗ No images found in M2 after migration!"); + } + $this->newLine(); + + // Step 7: Compare results + $this->info("Step 7: Comparison Results:"); + $this->line(" M1 images: " . count($m1Images)); + $this->line(" M2 images before: " . count($m2ImagesBefore)); + $this->line(" M2 images after: " . count($m2ImagesAfter)); + $this->line(" Expected: " . count($m1Images)); + $this->line(" Actual: " . count($m2ImagesAfter)); + + if (count($m2ImagesAfter) >= count($m1Images)) { + $this->info("✓ SUCCESS: All images migrated!"); + } else { + $this->error("✗ FAILURE: Missing " . (count($m1Images) - count($m2ImagesAfter)) . " images"); + + // Show which images are missing + $m1Files = array_column($m1Images, 'file'); + $m2Files = array_column($m2ImagesAfter, 'file'); + $missing = array_diff($m1Files, $m2Files); + + if (!empty($missing)) { + $this->error("Missing images:"); + foreach ($missing as $file) { + $this->line(" - {$file}"); + } + } + } + + return 0; + } + + protected function findM1ProductBySku($sku, $connection, $prefix) + { + // Try entity table first + try { + $product = DB::connection($connection) + ->table($prefix . 'catalog_product_entity') + ->where('sku', $sku) + ->first(); + + if ($product) { + return $product; + } + } catch (\Exception $e) { + // SKU might be in EAV table + } + + // Try EAV table + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $skuAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + if (!$skuAttributeId) { + return null; + } + + $skuRow = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('value', $sku) + ->where('store_id', 0) + ->first(); + + if (!$skuRow) { + return null; + } + + return DB::connection($connection) + ->table($prefix . 'catalog_product_entity') + ->where('entity_id', $skuRow->entity_id) + ->first(); + } + + protected function findM2ProductBySku($sku, $connection, $prefix) + { + // Try entity table first + try { + $product = DB::connection($connection) + ->table($prefix . 'catalog_product_entity') + ->where('sku', $sku) + ->first(); + + if ($product) { + return $product; + } + } catch (\Exception $e) { + // SKU might be in EAV table + } + + // Try EAV table + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return null; + } + + $skuAttributeId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'sku') + ->value('attribute_id'); + + if (!$skuAttributeId) { + return null; + } + + $skuRow = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_varchar') + ->where('attribute_id', $skuAttributeId) + ->where('value', $sku) + ->where('store_id', 0) + ->first(); + + if (!$skuRow) { + return null; + } + + return DB::connection($connection) + ->table($prefix . 'catalog_product_entity') + ->where('entity_id', $skuRow->entity_id) + ->first(); + } + + protected function getM1Images($productId, $connection, $prefix) + { + $images = []; + + // Check media gallery table + try { + $galleryImages = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_media_gallery') + ->where('entity_id', $productId) + ->get(); + + foreach ($galleryImages as $img) { + $images[] = [ + 'value_id' => $img->value_id, + 'file' => $img->value, + 'attribute_id' => $img->attribute_id ?? null, + ]; + } + } catch (\Exception $e) { + // Table might not exist + } + + // Also check individual image attributes + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if ($entityTypeId) { + $imageAttributes = ['image', 'small_image', 'thumbnail']; + foreach ($imageAttributes as $attrCode) { + $attrId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + + if ($attrId) { + $imageValue = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_varchar') + ->where('entity_id', $productId) + ->where('attribute_id', $attrId) + ->where('store_id', 0) + ->whereNotNull('value') + ->where('value', '!=', '') + ->where('value', '!=', 'no_selection') + ->value('value'); + + if ($imageValue && !in_array($imageValue, array_column($images, 'file'))) { + $images[] = [ + 'value_id' => null, + 'file' => $imageValue, + 'attribute_id' => $attrId, + ]; + } + } + } + } + + return $images; + } + + protected function getM2Images($productId, $connection, $prefix) + { + $images = []; + + // Get media gallery attribute ID + $entityTypeId = DB::connection($connection) + ->table($prefix . 'eav_entity_type') + ->where('entity_type_code', 'catalog_product') + ->value('entity_type_id'); + + if (!$entityTypeId) { + return $images; + } + + $mediaGalleryAttrId = DB::connection($connection) + ->table($prefix . 'eav_attribute') + ->where('entity_type_id', $entityTypeId) + ->where('attribute_code', 'media_gallery') + ->value('attribute_id'); + + if (!$mediaGalleryAttrId) { + return $images; + } + + // Get images linked to this product via value_to_entity table + $valueIds = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_media_gallery_value_to_entity') + ->where('entity_id', $productId) + ->pluck('value_id') + ->toArray(); + + if (empty($valueIds)) { + return $images; + } + + $galleryImages = DB::connection($connection) + ->table($prefix . 'catalog_product_entity_media_gallery') + ->whereIn('value_id', $valueIds) + ->where('attribute_id', $mediaGalleryAttrId) + ->get(); + + foreach ($galleryImages as $img) { + $images[] = [ + 'value_id' => $img->value_id, + 'file' => $img->value, + ]; + } + + return $images; + } +} + diff --git a/app/Services/MagentoCategoryMigrationService.php b/app/Services/MagentoCategoryMigrationService.php index bd4f4a1..968837b 100644 --- a/app/Services/MagentoCategoryMigrationService.php +++ b/app/Services/MagentoCategoryMigrationService.php @@ -2195,6 +2195,7 @@ public function migrateProducts($dryRun = false) // Migrate media gallery images (skip in dry run) if (!$dryRun) { + Log::info("Migrating images for product SKU: {$m1Sku}, M1 ID: {$m1Product->entity_id}, M2 ID: {$m2ProductId}"); $this->migrateProductMediaGallery($m1Product->entity_id, $m2ProductId, $m1EntityTypeId); } @@ -2515,6 +2516,7 @@ protected function migrateAllProductEntityData($m1ProductId, $m2ProductId, $m1En * Migrate product media gallery images from M1 to M2 * Populates catalog_product_entity_media_gallery, catalog_product_entity_media_gallery_value, * and catalog_product_entity_media_gallery_value_to_entity tables + * Also handles individual image attributes (image, small_image, thumbnail) */ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1EntityTypeId) { @@ -2526,11 +2528,6 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent ->where('attribute_code', 'media_gallery') ->value('attribute_id'); - if (!$m1MediaGalleryAttrId) { - // Media gallery attribute doesn't exist in M1, skip - return; - } - // Get media_gallery attribute ID from M2 $m2EntityTypeId = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'eav_entity_type') @@ -2553,45 +2550,145 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent } $images = []; + + // Also check for individual image attributes (image, small_image, thumbnail) + // These are stored as separate varchar attributes in M1 + $imageAttributes = ['image', 'small_image', 'thumbnail']; + $m1ImageAttributeIds = []; + foreach ($imageAttributes as $attrCode) { + $attrId = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'eav_attribute') + ->where('entity_type_id', $m1EntityTypeId) + ->where('attribute_code', $attrCode) + ->value('attribute_id'); + if ($attrId) { + $m1ImageAttributeIds[$attrCode] = $attrId; + } + } + + // Get individual image attribute values from M1 + if (!empty($m1ImageAttributeIds)) { + $m1ImageValues = DB::connection($this->magento1Connection) + ->table($this->magento1Prefix . 'catalog_product_entity_varchar') + ->where('entity_id', $m1ProductId) + ->whereIn('attribute_id', array_values($m1ImageAttributeIds)) + ->where('store_id', 0) + ->whereNotNull('value') + ->where('value', '!=', '') + ->where('value', '!=', 'no_selection') + ->get(); + + foreach ($m1ImageValues as $imageValue) { + $imageFile = $imageValue->value; + if (!empty($imageFile)) { + // Find which attribute this is + $attrCode = array_search($imageValue->attribute_id, $m1ImageAttributeIds); + // Add to images array if not already present + $found = false; + foreach ($images as $existingImage) { + if (isset($existingImage['file']) && $existingImage['file'] === $imageFile) { + $found = true; + break; + } + } + if (!$found) { + $images[] = [ + 'file' => $imageFile, + 'label' => ucfirst($attrCode ?? 'Image'), + 'position' => count($images) + 1, + 'disabled' => 0, + ]; + } + } + } + } // First, try to get images from M1 media gallery tables (if they exist) + // In M1, catalog_product_entity_media_gallery has: value_id, attribute_id, entity_id, value try { $m1MediaGalleryTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery'; $m1MediaGalleryValueTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery_value'; - // Check if M1 media gallery tables exist - $m1GalleryImages = DB::connection($this->magento1Connection) + // Check if M1 media gallery tables exist by trying to query them + // Filter by entity_id (product ID) - this is the key field + $m1GalleryQuery = DB::connection($this->magento1Connection) ->table($m1MediaGalleryTable) - ->where('entity_id', $m1ProductId) - ->get(); + ->where('entity_id', $m1ProductId); + + // Some M1 versions have attribute_id in the media_gallery table, try to filter by it if it exists + // But don't fail if the column doesn't exist + try { + if ($m1MediaGalleryAttrId) { + $m1GalleryQuery->where('attribute_id', $m1MediaGalleryAttrId); + } + } catch (Exception $e) { + // attribute_id column might not exist, continue without it + Log::debug("attribute_id column not found in M1 media gallery table, using entity_id only"); + } + + $m1GalleryImages = $m1GalleryQuery->get(); + + Log::info("Found " . $m1GalleryImages->count() . " images in M1 media gallery table for product ID {$m1ProductId} (SKU mapping: M1 ID {$m1ProductId} -> M2 ID {$m2ProductId})"); if ($m1GalleryImages->isNotEmpty()) { // M1 has media gallery tables, use them + Log::info("Processing " . $m1GalleryImages->count() . " images from M1 media gallery table"); foreach ($m1GalleryImages as $m1Image) { $valueId = $m1Image->value_id; - // Get value data - $m1Value = DB::connection($this->magento1Connection) - ->table($m1MediaGalleryValueTable) - ->where('value_id', $valueId) - ->where('store_id', 0) - ->first(); + // Get value data (label, position, disabled) from the value table + $m1Value = null; + try { + $m1Value = DB::connection($this->magento1Connection) + ->table($m1MediaGalleryValueTable) + ->where('value_id', $valueId) + ->where('store_id', 0) + ->first(); + } catch (Exception $e) { + // Value table might not exist or have different structure + Log::debug("Could not read from M1 media gallery value table: " . $e->getMessage()); + } - $images[] = [ - 'file' => $m1Image->value, - 'label' => $m1Value->label ?? null, - 'position' => $m1Value->position ?? 0, - 'disabled' => $m1Value->disabled ?? 0, - ]; + $imageFile = $m1Image->value ?? null; + if (empty($imageFile)) { + Log::warning("Image file is empty for value_id {$valueId} in M1 product {$m1ProductId}"); + continue; + } + + // Check if this image is already in the array (from individual attributes) + $found = false; + foreach ($images as $key => $existingImage) { + if (isset($existingImage['file']) && $existingImage['file'] === $imageFile) { + $found = true; + // Update with gallery data if it has better info + if ($m1Value && ($m1Value->label || $m1Value->position > 0)) { + $images[$key]['label'] = $m1Value->label ?? $images[$key]['label']; + $images[$key]['position'] = $m1Value->position ?? $images[$key]['position']; + $images[$key]['disabled'] = $m1Value->disabled ?? $images[$key]['disabled']; + } + break; + } + } + if (!$found) { + $images[] = [ + 'file' => $imageFile, + 'label' => $m1Value->label ?? null, + 'position' => $m1Value->position ?? 0, + 'disabled' => $m1Value->disabled ?? 0, + ]; + Log::debug("Added image from M1 media gallery: {$imageFile}"); + } } + } else { + Log::debug("No images found in M1 media gallery table for product ID {$m1ProductId}"); } } catch (Exception $e) { // M1 media gallery tables don't exist, fall back to varchar table - Log::debug("M1 media gallery tables not found, using varchar table: " . $e->getMessage()); + Log::warning("M1 media gallery tables not found or error accessing them, will try varchar table: " . $e->getMessage()); } // If no images from gallery tables, try varchar table (serialized data) - if (empty($images)) { + if (empty($images) && $m1MediaGalleryAttrId) { $m1MediaGalleryData = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'catalog_product_entity_varchar') ->where('entity_id', $m1ProductId) @@ -2600,8 +2697,10 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent ->value('value'); if (empty($m1MediaGalleryData)) { - // No media gallery data in M1, skip - return; + // No media gallery data in M1, but we might have individual images already + if (empty($images)) { + return; + } } // Parse the media gallery data (Magento 1 stores it as serialized PHP) @@ -2632,15 +2731,34 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent } foreach ($parsedImages as $img) { + $imageFile = null; if (is_string($img)) { - $images[] = ['file' => $img]; + $imageFile = $img; } elseif (is_array($img)) { - $images[] = [ - 'file' => $img['file'] ?? $img['value'] ?? null, - 'label' => $img['label'] ?? $img['label_default'] ?? null, - 'position' => isset($img['position']) ? (int)$img['position'] : 0, - 'disabled' => isset($img['disabled']) ? (int)$img['disabled'] : 0, - ]; + $imageFile = $img['file'] ?? $img['value'] ?? null; + } + + if (!empty($imageFile)) { + // Check if this image is already in the array + $found = false; + foreach ($images as $existingImage) { + if (isset($existingImage['file']) && $existingImage['file'] === $imageFile) { + $found = true; + break; + } + } + if (!$found) { + if (is_string($img)) { + $images[] = ['file' => $imageFile]; + } elseif (is_array($img)) { + $images[] = [ + 'file' => $imageFile, + 'label' => $img['label'] ?? $img['label_default'] ?? null, + 'position' => isset($img['position']) ? (int)$img['position'] : 0, + 'disabled' => isset($img['disabled']) ? (int)$img['disabled'] : 0, + ]; + } + } } } } @@ -2648,9 +2766,12 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent } if (empty($images)) { + Log::debug("No images found for product M1 ID {$m1ProductId} -> M2 ID {$m2ProductId}"); return; } + Log::info("Migrating " . count($images) . " images for product M1 ID {$m1ProductId} -> M2 ID {$m2ProductId}"); + // Process each image foreach ($images as $imageData) { // Handle different data structures @@ -2714,13 +2835,18 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent 'value_id' => $valueId, 'entity_id' => $m2ProductId, ]); + Log::debug("Linked image {$imageFile} (value_id: {$valueId}) to M2 product ID {$m2ProductId}"); + } else { + Log::debug("Image {$imageFile} (value_id: {$valueId}) already linked to M2 product ID {$m2ProductId}"); } // Insert/update catalog_product_entity_media_gallery_value for default store (store_id = 0) + // Note: This table requires entity_id as it has a foreign key constraint $valueExists = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value') ->where('value_id', $valueId) ->where('store_id', 0) + ->where('entity_id', $m2ProductId) ->exists(); if (!$valueExists) { @@ -2729,21 +2855,25 @@ protected function migrateProductMediaGallery($m1ProductId, $m2ProductId, $m1Ent ->insert([ 'value_id' => $valueId, 'store_id' => 0, + 'entity_id' => $m2ProductId, 'label' => $label, 'position' => $position, 'disabled' => $disabled, ]); + Log::debug("Inserted media gallery value for image {$imageFile} (value_id: {$valueId}, entity_id: {$m2ProductId})"); } else { // Update existing value DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value') ->where('value_id', $valueId) ->where('store_id', 0) + ->where('entity_id', $m2ProductId) ->update([ 'label' => $label, 'position' => $position, 'disabled' => $disabled, ]); + Log::debug("Updated media gallery value for image {$imageFile} (value_id: {$valueId}, entity_id: {$m2ProductId})"); } } diff --git a/resources/js/products.js b/resources/js/products.js index 5097174..30b1db2 100644 --- a/resources/js/products.js +++ b/resources/js/products.js @@ -18,15 +18,41 @@ document.addEventListener('DOMContentLoaded', function() { }); function startProductMigration(dryRun) { + // Check if routes are available + if (!routes || !routes.migrateProducts) { + console.error('Product routes not available', routes); + alert('Error: Routes not initialized. Please refresh the page.'); + return; + } + const button = dryRun ? document.getElementById('dryRunProductMigrationBtn') : document.getElementById('startProductMigrationBtn'); const otherButton = dryRun ? document.getElementById('startProductMigrationBtn') : document.getElementById('dryRunProductMigrationBtn'); + + if (!button) { + console.error('Button not found'); + alert('Error: Button not found. Please refresh the page.'); + return; + } + const originalText = button.textContent; button.disabled = true; - otherButton.disabled = true; + if (otherButton) { + otherButton.disabled = true; + } button.textContent = dryRun ? 'Running Dry Run...' : 'Migrating...'; button.style.cursor = 'not-allowed'; const logContent = document.getElementById('productMigrationLogContent'); + if (!logContent) { + console.error('Log content element not found'); + button.disabled = false; + if (otherButton) { + otherButton.disabled = false; + } + button.textContent = originalText; + return; + } + logContent.innerHTML = '
' + (dryRun ? 'Running dry run...' : 'Starting migration...') + '
'; fetch(routes.migrateProducts, {