migrate/app/Services/MagentoProductMigrationServ...

4963 lines
215 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Exception;
class MagentoProductMigrationService
{
protected $magento1Connection;
protected $magento2Connection;
protected $magento1Prefix;
protected $magento2Prefix;
protected $categoryService;
protected $storeMapping = [];
protected $migrationLog = [];
public function __construct()
{
$this->magento1Connection = 'magento1';
$this->magento2Connection = 'magento2';
$this->magento1Prefix = config('database.connections.magento1.prefix', '');
$this->magento2Prefix = config('database.connections.magento2.prefix', '');
}
/**
* Set the category service (for dependency injection)
*/
public function setCategoryService(MagentoCategoryMigrationService $categoryService)
{
$this->categoryService = $categoryService;
}
/**
* Get category mapping from category service
*/
protected function getCategoryMapping()
{
if ($this->categoryService) {
return $this->categoryService->getCategoryMapping();
}
return [];
}
/**
* Get all stores from Magento 1
*/
protected function getMagento1Stores()
{
try {
$stores = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'core_store')
->select('store_id', 'code', 'name', 'website_id', 'group_id')
->where('store_id', '>', 0)
->get();
return $stores;
} catch (Exception $e) {
Log::error('Error fetching Magento 1 stores: ' . $e->getMessage());
return collect([]);
}
}
/**
* Get all stores from Magento 2
*/
protected function getMagento2Stores()
{
try {
$stores = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'store')
->select('store_id', 'code', 'name', 'website_id')
->where('store_id', '>', 0)
->get();
return $stores;
} catch (Exception $e) {
Log::error('Error fetching Magento 2 stores: ' . $e->getMessage());
return collect([]);
}
}
// Product methods will be added here
public function getMagento1Products()
{
try {
// Get entity type ID for catalog_product
$entityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return collect([]);
}
// Get base product data - check if SKU column exists in entity table
$hasSkuColumn = false;
try {
$testQuery = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->select('sku')
->limit(1)
->first();
$hasSkuColumn = true;
} catch (Exception $e) {
// SKU column doesn't exist, will use EAV
$hasSkuColumn = false;
}
if ($hasSkuColumn) {
// SKU is stored directly in entity table
$products = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'has_options', 'required_options', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
} else {
// SKU is stored as EAV attribute
$products = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->select('entity_id', 'type_id', 'attribute_set_id', 'has_options', 'required_options', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
}
// Get SKU attribute ID (for EAV storage)
$skuAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'sku')
->value('attribute_id');
// Get name attribute ID
$nameAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
// Get SKU and name values from EAV if needed
$skus = [];
$names = [];
if (!$hasSkuColumn && $skuAttributeId) {
$skuValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $skuAttributeId)
->where('store_id', 0)
->pluck('value', 'entity_id')
->toArray();
$skus = $skuValues;
}
if ($nameAttributeId) {
$nameValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->pluck('value', 'entity_id')
->toArray();
$names = $nameValues;
}
// Combine data
foreach ($products as $product) {
if ($hasSkuColumn) {
// SKU from entity table
$product->sku = !empty($product->sku) ? $product->sku : 'N/A';
} else {
// SKU from EAV
$product->sku = $skus[$product->entity_id] ?? 'N/A';
}
$product->name = $names[$product->entity_id] ?? 'Unnamed Product';
}
return $products;
} catch (Exception $e) {
Log::error('Error fetching Magento 1 products: ' . $e->getMessage());
return collect([]);
}
}
/**
* Get all products from Magento 2
*/
public function getMagento2Products()
{
try {
// Get entity type ID for catalog_product
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return collect([]);
}
// Get base product data - check if SKU column exists in entity table
$hasSkuColumn = false;
try {
$testQuery = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->select('sku')
->limit(1)
->first();
$hasSkuColumn = true;
} catch (Exception $e) {
// SKU column doesn't exist, will use EAV
$hasSkuColumn = false;
}
if ($hasSkuColumn) {
// SKU is stored directly in entity table
$products = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->select('entity_id', 'sku', 'type_id', 'attribute_set_id', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
} else {
// SKU is stored as EAV attribute
$products = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->select('entity_id', 'type_id', 'attribute_set_id', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
}
// Get SKU attribute ID (for EAV storage)
$skuAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'sku')
->value('attribute_id');
// Get name attribute ID
$nameAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
// Get SKU and name values from EAV if needed
$skus = [];
$names = [];
if (!$hasSkuColumn && $skuAttributeId) {
$skuValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $skuAttributeId)
->where('store_id', 0)
->pluck('value', 'entity_id')
->toArray();
$skus = $skuValues;
}
if ($nameAttributeId) {
$nameValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->pluck('value', 'entity_id')
->toArray();
$names = $nameValues;
}
// Combine data
foreach ($products as $product) {
if ($hasSkuColumn) {
// SKU from entity table
$product->sku = !empty($product->sku) ? $product->sku : 'N/A';
} else {
// SKU from EAV
$product->sku = $skus[$product->entity_id] ?? 'N/A';
}
$product->name = $names[$product->entity_id] ?? 'Unnamed Product';
}
return $products;
} catch (Exception $e) {
Log::error('Error fetching Magento 2 products: ' . $e->getMessage());
return collect([]);
}
}
/**
* Get Magento 1 products that don't exist in Magento 2
*/
public function getM1ProductsNotInM2()
{
try {
$m1Products = $this->getMagento1Products();
$m2Products = $this->getMagento2Products();
// Get all M2 SKUs
$m2Skus = $m2Products->pluck('sku')->filter(function($sku) {
return $sku !== 'N/A' && !empty($sku);
})->toArray();
// Get M2 product IDs (for products without SKU)
$m2ProductIds = $m2Products->pluck('entity_id')->toArray();
// Filter M1 products that don't exist in M2
$missingProducts = $m1Products->filter(function ($m1Product) use ($m2Skus, $m2ProductIds) {
$m1Sku = $m1Product->sku ?? 'N/A';
$hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku));
if ($hasSku) {
// Check by SKU
return !in_array($m1Sku, $m2Skus);
} else {
// Check by product ID
return !in_array($m1Product->entity_id, $m2ProductIds);
}
});
return $missingProducts->values();
} catch (Exception $e) {
Log::error('Error fetching missing products: ' . $e->getMessage());
return collect([]);
}
}
/**
* Get Magento 2 products that don't exist in Magento 1
*/
public function getM2ProductsNotInM1()
{
try {
$m1Products = $this->getMagento1Products();
$m2Products = $this->getMagento2Products();
// Get all M1 SKUs
$m1Skus = $m1Products->pluck('sku')->filter(function($sku) {
return $sku !== 'N/A' && !empty($sku);
})->toArray();
// Get M1 product IDs (for products without SKU)
$m1ProductIds = $m1Products->pluck('entity_id')->toArray();
// Filter M2 products that don't exist in M1
$missingProducts = $m2Products->filter(function ($m2Product) use ($m1Skus, $m1ProductIds) {
$m2Sku = $m2Product->sku ?? 'N/A';
$hasSku = ($m2Sku !== 'N/A' && !empty($m2Sku));
if ($hasSku) {
// Check by SKU
return !in_array($m2Sku, $m1Skus);
} else {
// Check by product ID
return !in_array($m2Product->entity_id, $m1ProductIds);
}
});
return $missingProducts->values();
} catch (Exception $e) {
Log::error('Error fetching M2 products not in M1: ' . $e->getMessage());
return collect([]);
}
}
/**
* Migrate all products from Magento 1 to Magento 2
*/
/**
* Migrate all products from Magento 1 to Magento 2
*
* IMPORTANT: This migration only performs INSERT and UPDATE operations.
* NO DATA IS DELETED during migration. Existing data in M2 that doesn't
* exist in M1 will be preserved and not removed.
*
* @param bool $dryRun If true, only simulates the migration without making changes
* @return array Migration result with counts and logs
*/
public function migrateProducts($dryRun = false, $progressKey = null)
{
try {
$this->migrationLog = [];
$addedCount = 0;
$updatedCount = 0;
$errorCount = 0;
$missingAttributes = [];
// Get M1 and M2 entity type IDs
$m1EntityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$m1EntityTypeId || !$m2EntityTypeId) {
return [
'success' => false,
'message' => 'Entity type not found',
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => [],
'missing_attributes' => []
];
}
// Get all M1 products
$m1Products = $this->getMagento1Products();
$totalProducts = $m1Products->count();
// Initialize progress tracking if key provided
if ($progressKey) {
Cache::put($progressKey, [
'total' => $totalProducts,
'current' => 0,
'added' => 0,
'updated' => 0,
'errors' => 0,
'status' => 'running',
'current_sku' => ''
], 3600); // Store for 1 hour
}
// Get M2 attribute IDs (common ones)
$m2AttributeIds = $this->getMagento2ProductAttributeIds();
// Get M1 attribute IDs for common attributes
$m1AttributeIds = $this->getMagento1ProductAttributeIds();
// Get all M1 attributes to check for missing ones
$allM1Attributes = $this->getAllMagento1ProductAttributes();
// Also get all M1 attribute IDs (not just common ones) for comprehensive checking
$allM1AttributeIds = [];
if ($allM1Attributes) {
foreach ($allM1Attributes as $attr) {
$allM1AttributeIds[$attr->attribute_code] = $attr->attribute_id;
}
}
// Get all M2 attribute IDs for comparison
$allM2AttributeIds = $this->getAllMagento2ProductAttributeIds();
// Get category mapping (from previous category migrations)
$categoryMapping = $this->getCategoryMapping();
if (!$dryRun) {
DB::connection($this->magento2Connection)->beginTransaction();
}
// Check if M2 has SKU column in entity table
$m2HasSkuColumn = false;
try {
$testQuery = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->select('sku')
->limit(1)
->first();
$m2HasSkuColumn = true;
} catch (Exception $e) {
$m2HasSkuColumn = false;
}
$currentIndex = 0;
foreach ($m1Products as $m1Product) {
$currentIndex++;
try {
$m1Sku = $m1Product->sku ?? 'N/A';
$hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku));
// Update progress if tracking enabled
if ($progressKey) {
Cache::put($progressKey, [
'total' => $totalProducts,
'current' => $currentIndex,
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'status' => 'running',
'current_sku' => $m1Sku
], 3600);
}
// Check if product exists in M2 by SKU (if SKU exists) or by ID (if no SKU)
$m2Product = null;
$m2ProductId = null;
$isNew = false;
if ($hasSku) {
// Check if SKU exists in M2 - try both entity table and EAV
if ($m2HasSkuColumn) {
// SKU is stored in entity table
$m2Product = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('sku', $m1Sku)
->first();
} else {
// SKU is stored in EAV table
if (isset($m2AttributeIds['sku'])) {
$m2Product = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $m2AttributeIds['sku'])
->where('value', $m1Sku)
->where('store_id', 0)
->first();
}
}
} else {
// Product has no SKU, check by entity_id
$m2Product = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $m1Product->entity_id)
->first();
}
if ($m2Product) {
// Product exists, get its entity_id
$m2ProductId = $m2Product->entity_id;
if (!$dryRun) {
// Update has_options and required_options for existing products
$updateData = [
'has_options' => $m1Product->has_options ?? 0,
'required_options' => $m1Product->required_options ?? 0,
'updated_at' => now(),
];
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $m2ProductId)
->update($updateData);
$this->migrationLog[] = "Updating existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})";
} else {
$this->migrationLog[] = "Would update existing product: " . ($hasSku ? "SKU {$m1Sku}" : "No SKU") . " (ID: {$m2ProductId})";
}
$updatedCount++;
} else {
// Product doesn't exist
if ($hasSku) {
// Product has SKU, will get new ID
if (!$dryRun) {
// Create new product - check if we need to set SKU in entity table or EAV
if ($m2HasSkuColumn) {
// SKU goes in entity table
$m2ProductId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->insertGetId([
'sku' => $m1Sku,
'attribute_set_id' => $m1Product->attribute_set_id ?? 4,
'type_id' => $m1Product->type_id ?? 'simple',
'has_options' => $m1Product->has_options ?? 0,
'required_options' => $m1Product->required_options ?? 0,
'created_at' => $m1Product->created_at ?? now(),
'updated_at' => now(),
]);
} else {
// SKU goes in EAV table
$m2ProductId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->insertGetId([
'attribute_set_id' => $m1Product->attribute_set_id ?? 4,
'type_id' => $m1Product->type_id ?? 'simple',
'has_options' => $m1Product->has_options ?? 0,
'required_options' => $m1Product->required_options ?? 0,
'created_at' => $m1Product->created_at ?? now(),
'updated_at' => now(),
]);
// Insert SKU in EAV table
if (isset($m2AttributeIds['sku'])) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->insert([
'attribute_id' => $m2AttributeIds['sku'],
'store_id' => 0,
'entity_id' => $m2ProductId,
'value' => $m1Sku,
]);
}
}
$this->migrationLog[] = "Created new product: SKU {$m1Sku} (ID: {$m2ProductId})";
} else {
$m2ProductId = $m1Product->entity_id; // Use M1 ID for dry run simulation
$this->migrationLog[] = "Would create new product: SKU {$m1Sku} (ID: {$m2ProductId})";
}
} else {
// Product has no SKU, use M1 product ID
$m2ProductId = $m1Product->entity_id;
if (!$dryRun) {
// Check if this ID already exists in M2
$existing = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $m2ProductId)
->exists();
if (!$existing) {
$insertData = [
'entity_id' => $m2ProductId,
'attribute_set_id' => $m1Product->attribute_set_id ?? 4,
'type_id' => $m1Product->type_id ?? 'simple',
'has_options' => $m1Product->has_options ?? 0,
'required_options' => $m1Product->required_options ?? 0,
'created_at' => $m1Product->created_at ?? now(),
'updated_at' => now(),
];
// If SKU column exists, generate a SKU for products without one
$generatedSku = 'MIGRATED-' . $m2ProductId;
if ($m2HasSkuColumn) {
// Generate a unique SKU using entity_id to avoid null constraint violation
$insertData['sku'] = $generatedSku;
}
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->insert($insertData);
// If SKU is stored in EAV table, insert it there too
if (!$m2HasSkuColumn && isset($m2AttributeIds['sku'])) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->insert([
'attribute_id' => $m2AttributeIds['sku'],
'store_id' => 0,
'entity_id' => $m2ProductId,
'value' => $generatedSku,
]);
}
$this->migrationLog[] = "Created new product: Generated SKU {$generatedSku} (ID: {$m2ProductId}, using M1 ID)";
} else {
$this->migrationLog[] = "Updating existing product: No SKU (ID: {$m2ProductId})";
$updatedCount++;
$addedCount--; // Adjust counts
continue;
}
} else {
$this->migrationLog[] = "Would create new product: No SKU (ID: {$m2ProductId}, using M1 ID)";
}
}
$isNew = true;
$addedCount++;
}
// Migrate product attributes (with dry run support)
// Migrate ALL data from ALL catalog_product_entity_* tables
// This includes images, all store views, and all attributes
$missingAttrs = $this->migrateAllProductEntityData($m1Product->entity_id, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $allM1AttributeIds, $allM2AttributeIds, $dryRun, $allM1Attributes);
if ($missingAttrs) {
foreach ($missingAttrs as $attr) {
// Check if this attribute is already in the list
$exists = false;
foreach ($missingAttributes as $existingAttr) {
if ($existingAttr['code'] === $attr['code']) {
$exists = true;
break;
}
}
if (!$exists) {
$missingAttributes[] = $attr;
}
}
}
// 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);
}
// Migrate category associations (skip in dry run)
if (!$dryRun) {
// Ensure product is enabled and visible (required for products to show in categories after reindex)
$this->ensureProductIsEnabledAndVisible($m2ProductId);
$this->migrateProductCategories($m1Product->entity_id, $m2ProductId, $categoryMapping);
// Migrate URL rewrites for new products
if ($isNew) {
$this->migrateProductUrlRewrites($m1Product->entity_id, $m2ProductId);
}
// Migrate stock inventory for all products
$this->migrateProductStock($m1Product->entity_id, $m2ProductId);
// Migrate product website assignments for all products
$this->migrateProductWebsites($m1Product->entity_id, $m2ProductId);
}
} catch (Exception $e) {
$errorCount++;
$errorMsg = $e->getMessage();
Log::error("Error migrating product {$m1Product->entity_id} (SKU: " . ($m1Product->sku ?? 'N/A') . "): " . $errorMsg);
$this->migrationLog[] = "ERROR: Failed to migrate product SKU " . ($m1Product->sku ?? 'N/A') . ": " . $errorMsg;
// Try to extract missing attribute from error message
if (preg_match("/Table.*catalog_product_entity_(\w+).*doesn't exist/", $errorMsg, $matches)) {
$tableType = $matches[1] ?? null;
if ($tableType && $allM1Attributes) {
// Find attributes that use this backend type
foreach ($allM1Attributes as $attr) {
if ($attr->backend_type === $tableType) {
$missingAttr = [
'code' => $attr->attribute_code,
'label' => $attr->frontend_label ?? $attr->attribute_code,
'type' => $attr->backend_type
];
// Check if already exists
$exists = false;
foreach ($missingAttributes as $existingAttr) {
if ($existingAttr['code'] === $missingAttr['code']) {
$exists = true;
break;
}
}
if (!$exists) {
$missingAttributes[] = $missingAttr;
}
}
}
}
}
}
}
// Migrate catalog_product_option tables after all products are migrated
if (!$dryRun) {
$this->migrationLog[] = "Starting catalog_product_option tables migration...";
$optionMigrationResult = $this->migrateCatalogProductOptions();
if ($optionMigrationResult['migrated'] > 0 || $optionMigrationResult['errors'] > 0) {
$this->migrationLog[] = "Catalog product options migration: Migrated {$optionMigrationResult['migrated']} options, Errors: {$optionMigrationResult['errors']}";
}
}
if (!$dryRun) {
DB::connection($this->magento2Connection)->commit();
}
// Update final progress status if tracking enabled
if ($progressKey) {
Cache::put($progressKey, [
'total' => $totalProducts,
'current' => $totalProducts,
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'status' => 'completed',
'current_sku' => ''
], 3600);
}
return [
'success' => true,
'message' => $dryRun ? 'Product migration dry run completed' : 'Product migration completed',
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'log' => $this->migrationLog,
'missing_attributes' => $missingAttributes,
'dry_run' => $dryRun,
'progress_key' => $progressKey
];
} catch (Exception $e) {
if (!$dryRun && isset($this->magento2Connection)) {
DB::connection($this->magento2Connection)->rollBack();
}
// Update progress status to failed if tracking enabled
if (isset($progressKey) && $progressKey) {
Cache::put($progressKey, [
'total' => $totalProducts ?? 0,
'current' => $currentIndex ?? 0,
'added' => $addedCount ?? 0,
'updated' => $updatedCount ?? 0,
'errors' => $errorCount ?? 0,
'status' => 'failed',
'current_sku' => ''
], 3600);
}
Log::error('Product migration error: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Product migration failed: ' . $e->getMessage(),
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => [],
'missing_attributes' => [],
'progress_key' => $progressKey ?? null
];
}
}
/**
* Get Magento 1 product attribute IDs
*/
protected function getMagento1ProductAttributeIds()
{
$entityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return [];
}
$attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id'];
$attributeIds = [];
foreach ($attributes as $attrCode) {
$attrId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', $attrCode)
->value('attribute_id');
if ($attrId) {
$attributeIds[$attrCode] = $attrId;
}
}
return $attributeIds;
}
/**
* Get all Magento 1 product attributes
*/
protected function getAllMagento1ProductAttributes()
{
$entityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return collect([]);
}
return DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label')
->get();
}
/**
* Get all Magento 2 product attribute IDs
*/
protected function getAllMagento2ProductAttributeIds()
{
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return [];
}
$attributes = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->select('attribute_id', 'attribute_code')
->get();
$attributeIds = [];
foreach ($attributes as $attr) {
$attributeIds[$attr->attribute_code] = $attr->attribute_id;
}
return $attributeIds;
}
/**
* Get Magento 2 product attribute IDs
*/
protected function getMagento2ProductAttributeIds()
{
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return [];
}
$attributes = ['sku', 'name', 'description', 'short_description', 'price', 'weight', 'status', 'visibility', 'tax_class_id'];
$attributeIds = [];
foreach ($attributes as $attrCode) {
$attrId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', $attrCode)
->value('attribute_id');
if ($attrId) {
$attributeIds[$attrCode] = $attrId;
}
}
return $attributeIds;
}
/**
* Migrate ALL data from ALL catalog_product_entity_* tables for a product
* This ensures we migrate images, all store views, and all attributes
*
* IMPORTANT: This method only performs INSERT and UPDATE operations.
* NO DATA IS DELETED. Existing attribute values in M2 that don't exist
* in M1 will be preserved and not removed.
*/
protected function migrateAllProductEntityData($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $dryRun = false, $allM1Attributes = null)
{
$missingAttributes = [];
// All possible backend types for EAV attributes
$backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime'];
// Get attribute mapping: M1 attribute_id => M2 attribute_id
$attributeMapping = [];
if ($allM1Attributes) {
foreach ($allM1Attributes as $m1Attr) {
$attrCode = $m1Attr->attribute_code;
if (isset($m2AttributeIds[$attrCode])) {
$attributeMapping[$m1Attr->attribute_id] = [
'm2_attr_id' => $m2AttributeIds[$attrCode],
'backend_type' => $m1Attr->backend_type ?? 'varchar',
'attribute_code' => $attrCode
];
}
}
}
// Migrate data from each backend type table
foreach ($backendTypes as $backendType) {
$m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType;
$m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType;
try {
// Check if M1 table exists
$m1Rows = DB::connection($this->magento1Connection)
->table($m1Table)
->where('entity_id', $m1ProductId)
->get();
if ($m1Rows->isEmpty()) {
continue; // No data in this table for this product
}
// Check if M2 table exists
try {
DB::connection($this->magento2Connection)
->table($m2Table)
->limit(1)
->first();
} catch (Exception $e) {
Log::warning("M2 table {$m2Table} doesn't exist: " . $e->getMessage());
continue;
}
if (!$dryRun) {
// Migrate each row
foreach ($m1Rows as $m1Row) {
$m1AttrId = $m1Row->attribute_id;
// Check if we have a mapping for this attribute
if (!isset($attributeMapping[$m1AttrId])) {
// Attribute doesn't exist in M2, log it
if ($allM1Attributes) {
$attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId);
if ($attr) {
$missingAttr = [
'code' => $attr->attribute_code ?? 'unknown',
'label' => $attr->frontend_label ?? 'Unknown',
'type' => $attr->backend_type ?? $backendType
];
// Check if already in list
$exists = false;
foreach ($missingAttributes as $existingAttr) {
if ($existingAttr['code'] === $missingAttr['code']) {
$exists = true;
break;
}
}
if (!$exists) {
$missingAttributes[] = $missingAttr;
}
}
}
continue; // Skip this attribute
}
$m2AttrId = $attributeMapping[$m1AttrId]['m2_attr_id'];
$storeId = $m1Row->store_id ?? 0;
// Check if this row already exists in M2
$exists = DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2ProductId)
->where('attribute_id', $m2AttrId)
->where('store_id', $storeId)
->exists();
if ($exists) {
// Update existing row
DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2ProductId)
->where('attribute_id', $m2AttrId)
->where('store_id', $storeId)
->update(['value' => $m1Row->value]);
} else {
// Insert new row
DB::connection($this->magento2Connection)
->table($m2Table)
->insert([
'attribute_id' => $m2AttrId,
'store_id' => $storeId,
'entity_id' => $m2ProductId,
'value' => $m1Row->value,
]);
}
}
}
} catch (Exception $e) {
// Table doesn't exist in M1, skip it
Log::debug("M1 table {$m1Table} doesn't exist or error: " . $e->getMessage());
continue;
}
}
return $missingAttributes;
}
/**
* 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)
{
try {
// Get media_gallery attribute ID from M1
$m1MediaGalleryAttrId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $m1EntityTypeId)
->where('attribute_code', 'media_gallery')
->value('attribute_id');
// Get media_gallery attribute ID from M2
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$m2EntityTypeId) {
return;
}
$m2MediaGalleryAttrId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $m2EntityTypeId)
->where('attribute_code', 'media_gallery')
->value('attribute_id');
if (!$m2MediaGalleryAttrId) {
// Media gallery attribute doesn't exist in M2, skip
return;
}
$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 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);
// 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 (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());
}
$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::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) && $m1MediaGalleryAttrId) {
$m1MediaGalleryData = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_varchar')
->where('entity_id', $m1ProductId)
->where('attribute_id', $m1MediaGalleryAttrId)
->where('store_id', 0)
->value('value');
if (empty($m1MediaGalleryData)) {
// 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)
if (is_string($m1MediaGalleryData)) {
// Try to unserialize (Magento 1 format)
$unserialized = @unserialize($m1MediaGalleryData);
if ($unserialized !== false && is_array($unserialized)) {
$parsedImages = $unserialized;
} else {
// Try JSON decode (some versions might use JSON)
$jsonDecoded = @json_decode($m1MediaGalleryData, true);
if (is_array($jsonDecoded)) {
$parsedImages = $jsonDecoded;
} else {
// If it's a simple string, treat it as a single image path
if (!empty(trim($m1MediaGalleryData))) {
$parsedImages = [['file' => trim($m1MediaGalleryData)]];
} else {
$parsedImages = [];
}
}
}
// Convert parsed images to our format
if (!empty($parsedImages)) {
if (isset($parsedImages['images']) && is_array($parsedImages['images'])) {
$parsedImages = $parsedImages['images'];
}
foreach ($parsedImages as $img) {
$imageFile = null;
if (is_string($img)) {
$imageFile = $img;
} elseif (is_array($img)) {
$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,
];
}
}
}
}
}
}
}
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
$imageFile = null;
$label = null;
$position = 0;
$disabled = 0;
if (is_string($imageData)) {
// Simple string path
$imageFile = $imageData;
} elseif (is_array($imageData)) {
// Array structure
$imageFile = $imageData['file'] ?? $imageData['value'] ?? null;
$label = $imageData['label'] ?? $imageData['label_default'] ?? null;
$position = isset($imageData['position']) ? (int)$imageData['position'] : 0;
$disabled = isset($imageData['disabled']) ? (int)$imageData['disabled'] : 0;
}
if (empty($imageFile)) {
continue;
}
// Clean up the image file path (remove leading slashes, etc.)
$imageFile = ltrim($imageFile, '/');
if (empty($imageFile)) {
continue;
}
// Check if this image already exists in M2 for this product
$existingValueId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery')
->where('attribute_id', $m2MediaGalleryAttrId)
->where('value', $imageFile)
->value('value_id');
if (!$existingValueId) {
// Insert into catalog_product_entity_media_gallery
$valueId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery')
->insertGetId([
'attribute_id' => $m2MediaGalleryAttrId,
'value' => $imageFile,
]);
} else {
$valueId = $existingValueId;
}
// Check if value_to_entity link already exists
$linkExists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value_to_entity')
->where('value_id', $valueId)
->where('entity_id', $m2ProductId)
->exists();
if (!$linkExists) {
// Insert into catalog_product_entity_media_gallery_value_to_entity
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value_to_entity')
->insert([
'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) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value')
->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})");
}
}
} catch (Exception $e) {
// Log error but don't fail the entire migration
Log::warning("Error migrating media gallery for product M1 ID {$m1ProductId} -> M2 ID {$m2ProductId}: " . $e->getMessage());
}
}
/**
* Migrate product attributes from M1 to M2
*/
protected function migrateProductAttributes($m1ProductId, $m2ProductId, $m1EntityTypeId, $m2EntityTypeId, $m1AttributeIds, $m2AttributeIds, $isNew, $dryRun = false, $allM1Attributes = null)
{
$missingAttributes = [];
// Get attribute backend types for all M1 attributes
$m1AttributeTypes = [];
if (!empty($m1AttributeIds)) {
$m1AttributeTypes = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $m1EntityTypeId)
->whereIn('attribute_id', array_values($m1AttributeIds))
->pluck('backend_type', 'attribute_id')
->toArray();
}
$m2AttributeTypes = [];
if (!empty($m2AttributeIds)) {
$m2AttributeTypes = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $m2EntityTypeId)
->whereIn('attribute_id', array_values($m2AttributeIds))
->pluck('backend_type', 'attribute_id')
->toArray();
}
// Migrate each attribute
foreach ($m1AttributeIds as $attrCode => $m1AttrId) {
// Check if attribute exists in M2
if (!isset($m2AttributeIds[$attrCode])) {
// Attribute doesn't exist in M2
if ($allM1Attributes) {
$attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId);
if ($attr) {
$missingAttributes[] = [
'code' => $attrCode,
'label' => $attr->frontend_label ?? $attrCode,
'type' => $attr->backend_type ?? 'varchar'
];
}
}
continue;
}
$m2AttrId = $m2AttributeIds[$attrCode];
$backendType = $m1AttributeTypes[$m1AttrId] ?? 'varchar';
// Handle static attributes (stored in main entity table)
if ($backendType === 'static') {
// Static attributes are columns in catalog_product_entity table
// Skip for now as they're usually handled during entity creation
continue;
}
// Get M1 attribute value
$m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType;
try {
$m1Value = DB::connection($this->magento1Connection)
->table($m1Table)
->where('entity_id', $m1ProductId)
->where('attribute_id', $m1AttrId)
->where('store_id', 0)
->value('value');
} catch (Exception $e) {
// Table doesn't exist - this means the attribute table is missing
// Track this as a missing attribute/table
if ($allM1Attributes) {
$attr = $allM1Attributes->firstWhere('attribute_id', $m1AttrId);
if ($attr) {
$missingAttr = [
'code' => $attrCode,
'label' => $attr->frontend_label ?? $attrCode,
'type' => $backendType
];
// Check if already in list
$exists = false;
foreach ($missingAttributes as $existingAttr) {
if ($existingAttr['code'] === $missingAttr['code']) {
$exists = true;
break;
}
}
if (!$exists) {
$missingAttributes[] = $missingAttr;
}
}
}
Log::warning("Table {$m1Table} doesn't exist for attribute {$attrCode}: " . $e->getMessage());
continue;
}
if ($m1Value === null) {
continue; // No value in M1
}
if (!$dryRun) {
// Insert or update in M2
$m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType;
$exists = DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2ProductId)
->where('attribute_id', $m2AttrId)
->where('store_id', 0)
->exists();
if ($exists) {
DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2ProductId)
->where('attribute_id', $m2AttrId)
->where('store_id', 0)
->update(['value' => $m1Value]);
} else {
DB::connection($this->magento2Connection)
->table($m2Table)
->insert([
'attribute_id' => $m2AttrId,
'store_id' => 0,
'entity_id' => $m2ProductId,
'value' => $m1Value,
]);
}
}
}
return $missingAttributes;
}
/**
* Compare M1 and M2 product values by SKU
* Returns all differences that would be updated during migration
*/
public function compareProductBySku($sku)
{
try {
$comparisonByTable = [];
// Get M1 and M2 entity type IDs
$m1EntityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$m1EntityTypeId || !$m2EntityTypeId) {
return [
'success' => false,
'message' => 'Entity type not found',
'changes_by_table' => []
];
}
// Find M1 product by SKU
$m1Product = $this->findProductBySku($sku, $this->magento1Connection, $this->magento1Prefix);
if (!$m1Product) {
return [
'success' => false,
'message' => "Product with SKU '{$sku}' not found in Magento 1",
'changes_by_table' => []
];
}
$m1ProductId = $m1Product->entity_id;
// Find M2 product by SKU
$m2Product = $this->findProductBySku($sku, $this->magento2Connection, $this->magento2Prefix);
if (!$m2Product) {
return [
'success' => true,
'message' => "Product with SKU '{$sku}' not found in Magento 2. It will be created during migration.",
'm1_product_id' => $m1ProductId,
'm2_product_id' => null,
'changes_by_table' => []
];
}
$m2ProductId = $m2Product->entity_id;
// Get all M1 attributes
$allM1Attributes = $this->getAllMagento1ProductAttributes();
$m2AttributeIds = $this->getAllMagento2ProductAttributeIds();
// Build attribute mapping
$attributeMapping = [];
if ($allM1Attributes) {
foreach ($allM1Attributes as $m1Attr) {
$attrCode = $m1Attr->attribute_code;
if (isset($m2AttributeIds[$attrCode])) {
$attributeMapping[$m1Attr->attribute_id] = [
'm2_attr_id' => $m2AttributeIds[$attrCode],
'backend_type' => $m1Attr->backend_type ?? 'varchar',
'attribute_code' => $attrCode,
'frontend_label' => $m1Attr->frontend_label ?? $attrCode
];
}
}
}
// Compare all backend type tables
$backendTypes = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($backendTypes as $backendType) {
$m1Table = $this->magento1Prefix . 'catalog_product_entity_' . $backendType;
$m2Table = $this->magento2Prefix . 'catalog_product_entity_' . $backendType;
$tableChanges = [];
try {
// Get M1 values
$m1Rows = DB::connection($this->magento1Connection)
->table($m1Table)
->where('entity_id', $m1ProductId)
->get();
// Get M2 values
$m2Rows = DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2ProductId)
->get();
// Build M2 lookup by attribute_id and store_id
$m2Lookup = [];
foreach ($m2Rows as $m2Row) {
$key = $m2Row->attribute_id . '_' . ($m2Row->store_id ?? 0);
$m2Lookup[$key] = $m2Row;
}
// Compare M1 values with M2
foreach ($m1Rows as $m1Row) {
$m1AttrId = $m1Row->attribute_id;
// Check if attribute exists in M2
if (!isset($attributeMapping[$m1AttrId])) {
continue; // Skip attributes that don't exist in M2
}
$mapping = $attributeMapping[$m1AttrId];
$m2AttrId = $mapping['m2_attr_id'];
$storeId = $m1Row->store_id ?? 0;
$key = $m2AttrId . '_' . $storeId;
$m1Value = $m1Row->value ?? '';
$m2Value = isset($m2Lookup[$key]) ? ($m2Lookup[$key]->value ?? '') : null;
// Check if values are different
if ($m1Value != $m2Value) {
$tableChanges[] = [
'attribute_code' => $mapping['attribute_code'],
'attribute_label' => $mapping['frontend_label'],
'backend_type' => $mapping['backend_type'],
'store_id' => $storeId,
'm1_value' => $m1Value,
'm2_value' => $m2Value,
'action' => $m2Value === null ? 'INSERT' : 'UPDATE'
];
}
}
// Check for M2 values that don't exist in M1 (would be deleted)
foreach ($m2Rows as $m2Row) {
$m2AttrId = $m2Row->attribute_id;
$storeId = $m2Row->store_id ?? 0;
// Check if this attribute exists in M1 mapping
$foundInM1 = false;
foreach ($attributeMapping as $m1AttrId => $mapping) {
if ($mapping['m2_attr_id'] == $m2AttrId) {
// Check if M1 has this value
$m1HasValue = $m1Rows->where('attribute_id', $m1AttrId)
->where('store_id', $storeId)
->isNotEmpty();
if ($m1HasValue) {
$foundInM1 = true;
break;
}
}
}
// If not found in M1 and it's not a system attribute, note it
if (!$foundInM1) {
// Try to get attribute code for display
$attrCode = 'unknown';
foreach ($attributeMapping as $m1AttrId => $mapping) {
if ($mapping['m2_attr_id'] == $m2AttrId) {
$attrCode = $mapping['attribute_code'];
break;
}
}
// Only add if it's a meaningful attribute (not system-generated)
if ($attrCode !== 'unknown') {
$tableChanges[] = [
'attribute_code' => $attrCode,
'attribute_label' => 'Unknown',
'backend_type' => $backendType,
'store_id' => $storeId,
'm1_value' => null,
'm2_value' => $m2Row->value ?? '',
'action' => 'DELETE'
];
}
}
}
// Add table changes to comparison (even if empty, to show all tables)
$comparisonByTable[$backendType] = [
'table_name' => 'catalog_product_entity_' . $backendType,
'm1_table' => $m1Table,
'm2_table' => $m2Table,
'changes' => $tableChanges
];
} catch (Exception $e) {
// Table might not exist, add empty entry
$comparisonByTable[$backendType] = [
'table_name' => 'catalog_product_entity_' . $backendType,
'm1_table' => $m1Table,
'm2_table' => $m2Table,
'changes' => [],
'error' => $e->getMessage()
];
Log::debug("Table comparison error for {$m1Table}: " . $e->getMessage());
continue;
}
}
return [
'success' => true,
'message' => "Comparison completed for SKU '{$sku}'",
'm1_product_id' => $m1ProductId,
'm2_product_id' => $m2ProductId,
'sku' => $sku,
'changes_by_table' => $comparisonByTable
];
} catch (Exception $e) {
Log::error('Error comparing product by SKU: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Comparison failed: ' . $e->getMessage(),
'changes' => []
];
}
}
/**
* Find product by SKU in either M1 or M2
*/
protected function findProductBySku($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();
}
/**
* Delete a single product from Magento 2
*/
public function deleteM2Product($productId)
{
try {
// Check if product exists
$product = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $productId)
->first();
if (!$product) {
return [
'success' => false,
'message' => 'Product not found in Magento 2'
];
}
DB::connection($this->magento2Connection)->beginTransaction();
try {
// Delete product attributes
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType;
try {
DB::connection($this->magento2Connection)
->table($table)
->where('entity_id', $productId)
->delete();
} catch (Exception $e) {
// Table might not exist, continue
Log::warning("Table {$table} might not exist: " . $e->getMessage());
}
}
// Delete category associations
try {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('product_id', $productId)
->delete();
} catch (Exception $e) {
Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage());
}
// Delete stock items if they exist
try {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->where('product_id', $productId)
->delete();
} catch (Exception $e) {
// Table might not exist, continue
}
// Delete the product entity
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $productId)
->delete();
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => "Product ID {$productId} deleted successfully"
];
} catch (Exception $e) {
DB::connection($this->magento2Connection)->rollBack();
throw $e;
}
} catch (Exception $e) {
Log::error('Error deleting M2 product: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to delete product: ' . $e->getMessage()
];
}
}
/**
* Sync product category assignments from M1 to M2
* Updates M2 products to be in the same categories as their M1 counterparts
*/
public function syncProductCategories()
{
try {
$this->migrationLog = [];
$updatedCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Get category mapping
$categoryMapping = $this->getCategoryMapping();
if (empty($categoryMapping)) {
return [
'success' => false,
'message' => 'No category mapping found. Please migrate categories first.',
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => []
];
}
// Get all M1 products with their category associations
$m1Products = $this->getMagento1Products();
$m2Products = $this->getMagento2Products();
// Create lookup maps for M2 products
$m2ProductBySku = [];
$m2ProductById = [];
foreach ($m2Products as $m2Product) {
$sku = $m2Product->sku ?? 'N/A';
if ($sku !== 'N/A' && !empty($sku)) {
$m2ProductBySku[$sku] = $m2Product->entity_id;
}
$m2ProductById[$m2Product->entity_id] = $m2Product->entity_id;
}
DB::connection($this->magento2Connection)->beginTransaction();
foreach ($m1Products as $m1Product) {
try {
// Find corresponding M2 product
$m2ProductId = null;
$m1Sku = $m1Product->sku ?? 'N/A';
if ($m1Sku !== 'N/A' && !empty($m1Sku)) {
// Match by SKU
if (isset($m2ProductBySku[$m1Sku])) {
$m2ProductId = $m2ProductBySku[$m1Sku];
}
} else {
// Match by product ID
if (isset($m2ProductById[$m1Product->entity_id])) {
$m2ProductId = $m1Product->entity_id;
}
}
if (!$m2ProductId) {
$skippedCount++;
$this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - not found in M2";
continue;
}
// Ensure product is enabled and visible (required for products to show in categories after reindex)
$this->ensureProductIsEnabledAndVisible($m2ProductId);
// Get M1 product categories
$m1Categories = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_category_product')
->where('product_id', $m1Product->entity_id)
->get();
if ($m1Categories->isEmpty()) {
$skippedCount++;
$this->migrationLog[] = "SKIPPED: M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}) - no categories in M1";
continue;
}
// Get current M2 product categories
$m2CurrentCategories = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('product_id', $m2ProductId)
->pluck('category_id')
->toArray();
// Build expected M2 categories from M1 categories using mapping
$expectedM2Categories = [];
$categoriesToAdd = [];
$unmappedCategories = [];
foreach ($m1Categories as $m1Category) {
if (isset($categoryMapping[$m1Category->category_id])) {
$m2CategoryId = $categoryMapping[$m1Category->category_id];
$expectedM2Categories[] = $m2CategoryId;
// Check if this category association needs to be added
if (!in_array($m2CategoryId, $m2CurrentCategories)) {
$categoriesToAdd[] = [
'category_id' => $m2CategoryId,
'product_id' => $m2ProductId,
'position' => $m1Category->position ?? 0,
];
}
} else {
// Category not in mapping - try to find it by name
$m1CategoryName = $this->getCategoryNameById($m1Category->category_id, 'm1');
if ($m1CategoryName) {
$m2CategoryId = $this->findCategoryByName($m1CategoryName, 'm2');
if ($m2CategoryId) {
// Found by name, add to mapping for future use
$categoryMapping[$m1Category->category_id] = $m2CategoryId;
$expectedM2Categories[] = $m2CategoryId;
if (!in_array($m2CategoryId, $m2CurrentCategories)) {
$categoriesToAdd[] = [
'category_id' => $m2CategoryId,
'product_id' => $m2ProductId,
'position' => $m1Category->position ?? 0,
];
}
$this->migrationLog[] = "FOUND BY NAME: M1 Category ID {$m1Category->category_id} ({$m1CategoryName}) -> M2 Category ID {$m2CategoryId}";
} else {
$unmappedCategories[] = $m1CategoryName ?: "ID {$m1Category->category_id}";
}
} else {
$unmappedCategories[] = "ID {$m1Category->category_id}";
}
}
}
if (!empty($unmappedCategories)) {
$this->migrationLog[] = "WARNING: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - M1 categories not found in M2: " . implode(', ', $unmappedCategories);
}
// Remove categories that are in M2 but not in M1 (after mapping)
$categoriesToRemove = array_diff($m2CurrentCategories, $expectedM2Categories);
// Add missing category associations
foreach ($categoriesToAdd as $categoryData) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->insert($categoryData);
}
// Remove categories that shouldn't be there
if (!empty($categoriesToRemove)) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('product_id', $m2ProductId)
->whereIn('category_id', $categoriesToRemove)
->delete();
}
if (!empty($categoriesToAdd) || !empty($categoriesToRemove)) {
$updatedCount++;
$addedCount = count($categoriesToAdd);
$removedCount = count($categoriesToRemove);
$this->migrationLog[] = "UPDATED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - Added {$addedCount} category(ies), Removed {$removedCount} category(ies)";
} else {
$skippedCount++;
$this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m1Sku}) - categories already match";
}
} catch (Exception $e) {
$errorCount++;
$m1Sku = $m1Product->sku ?? 'N/A';
$this->migrationLog[] = "ERROR: Failed to sync categories for M1 Product ID {$m1Product->entity_id} (SKU: {$m1Sku}): " . $e->getMessage();
Log::error("Error syncing product categories for M1 Product ID {$m1Product->entity_id}: " . $e->getMessage());
}
}
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => "Synced product categories. Updated: {$updatedCount}, Skipped: {$skippedCount}, Errors: {$errorCount}",
'updated' => $updatedCount,
'skipped' => $skippedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
} catch (Exception $e) {
if (isset($this->magento2Connection)) {
DB::connection($this->magento2Connection)->rollBack();
}
Log::error('Error syncing product categories: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to sync product categories: ' . $e->getMessage(),
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => $this->migrationLog
];
}
}
/**
* Delete all products in Magento 2 that have entity_id greater than max M1 product ID
*/
public function deleteM2ProductsAboveM1Max()
{
try {
// Get max product ID from M1
$maxM1ProductId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->max('entity_id');
if (!$maxM1ProductId) {
return [
'success' => false,
'message' => 'No products found in Magento 1',
'deleted' => 0
];
}
// Get products to delete from M2
$productsToDelete = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', '>', $maxM1ProductId)
->pluck('entity_id')
->toArray();
if (empty($productsToDelete)) {
return [
'success' => true,
'message' => 'No products found to delete',
'deleted' => 0,
'max_m1_id' => $maxM1ProductId
];
}
$deletedCount = 0;
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
DB::connection($this->magento2Connection)->beginTransaction();
foreach ($productsToDelete as $productId) {
try {
// Delete product attributes
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$table = $this->magento2Prefix . 'catalog_product_entity_' . $tableType;
try {
DB::connection($this->magento2Connection)
->table($table)
->where('entity_id', $productId)
->delete();
} catch (Exception $e) {
// Table might not exist, continue
Log::warning("Table {$table} might not exist: " . $e->getMessage());
}
}
// Delete category associations
try {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('product_id', $productId)
->delete();
} catch (Exception $e) {
Log::warning("Table {$this->magento2Prefix}catalog_category_product might not exist: " . $e->getMessage());
}
// Delete stock items if they exist
try {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->where('product_id', $productId)
->delete();
} catch (Exception $e) {
// Table might not exist, continue
}
// Delete the product entity
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->where('entity_id', $productId)
->delete();
$deletedCount++;
} catch (Exception $e) {
Log::error("Error deleting product {$productId}: " . $e->getMessage());
}
}
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => "Deleted {$deletedCount} product(s) with ID > {$maxM1ProductId}",
'deleted' => $deletedCount,
'max_m1_id' => $maxM1ProductId
];
} catch (Exception $e) {
if (isset($this->magento2Connection)) {
DB::connection($this->magento2Connection)->rollBack();
}
Log::error('Error deleting M2 products above M1 max: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to delete products: ' . $e->getMessage(),
'deleted' => 0
];
}
}
/**
* Migrate product category associations
*
* IMPORTANT: This method only performs INSERT operations.
* NO DATA IS DELETED. Existing category associations in M2 will be preserved.
*/
protected function migrateProductCategories($m1ProductId, $m2ProductId, $categoryMapping)
{
// Get M1 product categories
$m1Categories = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_category_product')
->where('product_id', $m1ProductId)
->get();
foreach ($m1Categories as $m1Category) {
// Find corresponding M2 category
if (isset($categoryMapping[$m1Category->category_id])) {
$m2CategoryId = $categoryMapping[$m1Category->category_id];
// Check if association already exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('category_id', $m2CategoryId)
->where('product_id', $m2ProductId)
->exists();
if (!$exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->insert([
'category_id' => $m2CategoryId,
'product_id' => $m2ProductId,
'position' => $m1Category->position ?? 0,
]);
}
}
}
}
/**
* Get store mapping by matching store codes
*/
protected function getStoreMapping()
{
// If store mapping was set, use it
if (!empty($this->storeMapping)) {
return $this->storeMapping;
}
// Try to get from category service if available
if ($this->categoryService && method_exists($this->categoryService, 'getStoreMapping')) {
$mapping = $this->categoryService->getStoreMapping();
if (!empty($mapping)) {
return $mapping;
}
}
// Otherwise, try to build mapping by matching store codes
$mapping = [];
try {
$m1Stores = $this->getMagento1Stores();
$m2Stores = $this->getMagento2Stores();
// Create a map of M2 stores by code
$m2StoreMap = [];
foreach ($m2Stores as $m2Store) {
$code = strtolower(trim($m2Store->code ?? ''));
if (!empty($code) && !isset($m2StoreMap[$code])) {
$m2StoreMap[$code] = $m2Store->store_id;
}
}
// Match M1 stores to M2 by code
foreach ($m1Stores as $m1Store) {
$code = strtolower(trim($m1Store->code ?? ''));
if (!empty($code) && isset($m2StoreMap[$code])) {
$mapping[$m1Store->store_id] = $m2StoreMap[$code];
} else {
// If no match by code, try to match by store_id (default store)
// This is a fallback for stores with same ID
$mapping[$m1Store->store_id] = $m1Store->store_id;
}
}
} catch (Exception $e) {
Log::warning('Error building store mapping: ' . $e->getMessage());
}
return $mapping;
}
/**
* Get website mapping by matching website codes or IDs from stores
*/
protected function getWebsiteMapping()
{
$mapping = [];
try {
$m1Stores = $this->getMagento1Stores();
$m2Stores = $this->getMagento2Stores();
// Build website mapping from store website_ids
// Group stores by website_id and match by store codes
$m1WebsiteMap = [];
foreach ($m1Stores as $m1Store) {
$websiteId = $m1Store->website_id ?? 0;
if ($websiteId > 0 && !isset($m1WebsiteMap[$websiteId])) {
$m1WebsiteMap[$websiteId] = $m1Store->code ?? '';
}
}
$m2WebsiteMap = [];
foreach ($m2Stores as $m2Store) {
$websiteId = $m2Store->website_id ?? 0;
if ($websiteId > 0 && !isset($m2WebsiteMap[$websiteId])) {
$m2WebsiteMap[$websiteId] = $m2Store->code ?? '';
}
}
// Match websites by store codes (websites with stores that have matching codes)
foreach ($m1WebsiteMap as $m1WebsiteId => $m1StoreCode) {
$m1StoreCodeLower = strtolower(trim($m1StoreCode));
if (!empty($m1StoreCodeLower)) {
// Find M2 website with matching store code
foreach ($m2WebsiteMap as $m2WebsiteId => $m2StoreCode) {
$m2StoreCodeLower = strtolower(trim($m2StoreCode));
if ($m1StoreCodeLower === $m2StoreCodeLower) {
$mapping[$m1WebsiteId] = $m2WebsiteId;
break;
}
}
}
// If no match found, try to match by website_id (fallback)
if (!isset($mapping[$m1WebsiteId])) {
// Check if M2 has a website with the same ID
if (isset($m2WebsiteMap[$m1WebsiteId])) {
$mapping[$m1WebsiteId] = $m1WebsiteId;
} else {
// Default to website ID 1 (main website) if no match
$mapping[$m1WebsiteId] = 1;
}
}
}
// If no mapping found, ensure at least default website (ID 1) is mapped
if (empty($mapping)) {
$mapping[1] = 1; // Default website mapping
}
} catch (Exception $e) {
Log::warning('Error building website mapping: ' . $e->getMessage());
// Default to website ID 1 if error
$mapping[1] = 1;
}
return $mapping;
}
/**
* Migrate product website assignments from M1 catalog_product_website to M2 catalog_product_website
*
* IMPORTANT: This method only performs INSERT operations.
* NO DATA IS DELETED. Existing website assignments in M2 will be preserved.
*/
protected function migrateProductWebsites($m1ProductId, $m2ProductId)
{
try {
// Get website mapping
$websiteMapping = $this->getWebsiteMapping();
// Get product websites from M1
$m1ProductWebsites = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_website')
->where('product_id', $m1ProductId)
->get();
if ($m1ProductWebsites->isEmpty()) {
// No websites assigned in M1, assign to default website (ID 1)
$this->assignProductToWebsite($m2ProductId, 1);
return;
}
// Migrate each website assignment
foreach ($m1ProductWebsites as $m1ProductWebsite) {
$m1WebsiteId = $m1ProductWebsite->website_id ?? 1;
$m2WebsiteId = isset($websiteMapping[$m1WebsiteId]) ? $websiteMapping[$m1WebsiteId] : 1;
// Check if assignment already exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_website')
->where('product_id', $m2ProductId)
->where('website_id', $m2WebsiteId)
->exists();
if (!$exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_website')
->insert([
'product_id' => $m2ProductId,
'website_id' => $m2WebsiteId,
]);
}
}
} catch (Exception $e) {
Log::error("Error migrating product websites for M1 ID {$m1ProductId}, M2 ID {$m2ProductId}: " . $e->getMessage());
// Don't throw - just log the error and assign to default website
try {
$this->assignProductToWebsite($m2ProductId, 1);
} catch (Exception $e2) {
Log::error("Error assigning product to default website: " . $e2->getMessage());
}
}
}
/**
* Assign a product to a website
*/
protected function assignProductToWebsite($productId, $websiteId)
{
try {
// Check if assignment already exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_website')
->where('product_id', $productId)
->where('website_id', $websiteId)
->exists();
if (!$exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_website')
->insert([
'product_id' => $productId,
'website_id' => $websiteId,
]);
}
} catch (Exception $e) {
Log::error("Error assigning product {$productId} to website {$websiteId}: " . $e->getMessage());
}
}
/**
* Migrate URL rewrites from M1 core_url_rewrite to M2 url_rewrite for a product
*/
protected function migrateProductUrlRewrites($m1ProductId, $m2ProductId)
{
try {
// Get store mapping
$storeMapping = $this->getStoreMapping();
// Get all URL rewrites for this product from M1
// In M1, product URLs are identified by id_path like "product/{product_id}" or "product/{product_id}/..."
$m1UrlRewrites = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'core_url_rewrite')
->where(function($query) use ($m1ProductId) {
$query->where('id_path', '=', 'product/' . $m1ProductId)
->orWhere('id_path', 'like', 'product/' . $m1ProductId . '/%');
})
->get();
if ($m1UrlRewrites->isEmpty()) {
return;
}
foreach ($m1UrlRewrites as $m1Rewrite) {
// Map store ID
$m1StoreId = $m1Rewrite->store_id ?? 0;
$m2StoreId = isset($storeMapping[$m1StoreId]) ? $storeMapping[$m1StoreId] : $m1StoreId;
// Extract request_path and target_path from M1
$requestPath = $m1Rewrite->request_path ?? '';
$targetPath = $m1Rewrite->target_path ?? '';
// Skip if paths are empty
if (empty($requestPath) || empty($targetPath)) {
continue;
}
// Update target_path to use M2 product ID
// M1 target_path format: catalog/product/view/id/{product_id}
// M2 target_path format: catalog/product/view/id/{product_id}
$targetPath = preg_replace('/\/id\/\d+/', '/id/' . $m2ProductId, $targetPath);
// Check if this URL rewrite already exists in M2
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'url_rewrite')
->where('entity_type', 'product')
->where('entity_id', $m2ProductId)
->where('request_path', $requestPath)
->where('store_id', $m2StoreId)
->exists();
if (!$exists) {
// Insert URL rewrite into M2
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'url_rewrite')
->insert([
'entity_type' => 'product',
'entity_id' => $m2ProductId,
'request_path' => $requestPath,
'target_path' => $targetPath,
'redirect_type' => 0, // 0 = No redirect
'store_id' => $m2StoreId,
'description' => $m1Rewrite->description ?? null,
'is_autogenerated' => ($m1Rewrite->is_system ?? 0) ? 1 : 0, // M1 is_system=1 means auto-generated, same as M2
'metadata' => null,
]);
}
}
} catch (Exception $e) {
Log::error("Error migrating URL rewrites for product M1 ID {$m1ProductId}, M2 ID {$m2ProductId}: " . $e->getMessage());
// Don't throw - just log the error and continue
}
}
/**
* Migrate stock inventory from M1 cataloginventory_stock_item to M2 cataloginventory_stock_item
*/
/**
* Migrate product stock inventory from M1 to M2
*
* IMPORTANT: This method only performs INSERT and UPDATE operations.
* NO DATA IS DELETED. Existing stock data in M2 will be preserved or updated.
*/
protected function migrateProductStock($m1ProductId, $m2ProductId)
{
try {
// Get stock item from M1
$m1StockItem = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'cataloginventory_stock_item')
->where('product_id', $m1ProductId)
->first();
if (!$m1StockItem) {
// No stock record in M1, create a default one in M2
$this->createDefaultStockItem($m2ProductId);
return;
}
// Check if stock item already exists in M2
$existingStockItem = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->where('product_id', $m2ProductId)
->first();
// Prepare stock data for M2
$stockData = [
'product_id' => $m2ProductId,
'stock_id' => $m1StockItem->stock_id ?? 1, // Default to 1 if not set
'qty' => $m1StockItem->qty ?? 0,
'is_in_stock' => $m1StockItem->is_in_stock ?? 0,
'manage_stock' => $m1StockItem->manage_stock ?? 1,
'use_config_manage_stock' => $m1StockItem->use_config_manage_stock ?? 1,
'min_qty' => $m1StockItem->min_qty ?? 0,
'min_sale_qty' => $m1StockItem->min_sale_qty ?? 1,
'max_sale_qty' => $m1StockItem->max_sale_qty ?? 0,
'is_qty_decimal' => $m1StockItem->is_qty_decimal ?? 0,
'backorders' => $m1StockItem->backorders ?? 0,
'notify_stock_qty' => $m1StockItem->notify_stock_qty ?? 0,
'use_config_notify_stock_qty' => $m1StockItem->use_config_notify_stock_qty ?? 1,
'use_config_min_qty' => $m1StockItem->use_config_min_qty ?? 1,
'use_config_min_sale_qty' => $m1StockItem->use_config_min_sale_qty ?? 1,
'use_config_max_sale_qty' => $m1StockItem->use_config_max_sale_qty ?? 1,
'use_config_backorders' => $m1StockItem->use_config_backorders ?? 1,
'use_config_enable_qty_inc' => $m1StockItem->use_config_enable_qty_inc ?? 1,
'enable_qty_increments' => $m1StockItem->enable_qty_increments ?? 0,
'use_config_qty_increments' => $m1StockItem->use_config_qty_increments ?? 1,
'qty_increments' => $m1StockItem->qty_increments ?? 0,
];
// Add optional fields if they exist in M1
if (isset($m1StockItem->low_stock_date)) {
$stockData['low_stock_date'] = $m1StockItem->low_stock_date;
}
if (isset($m1StockItem->is_decimal_divided)) {
$stockData['is_decimal_divided'] = $m1StockItem->is_decimal_divided;
}
if ($existingStockItem) {
// Update existing stock item
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->where('product_id', $m2ProductId)
->update($stockData);
} else {
// Insert new stock item
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->insert($stockData);
}
// Also update cataloginventory_stock_status if it exists
try {
$stockStatusData = [
'product_id' => $m2ProductId,
'website_id' => 0, // Default website
'stock_id' => $m1StockItem->stock_id ?? 1,
'qty' => $m1StockItem->qty ?? 0,
'stock_status' => $m1StockItem->is_in_stock ?? 0,
];
$existingStatus = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_status')
->where('product_id', $m2ProductId)
->where('website_id', 0)
->where('stock_id', $stockStatusData['stock_id'])
->first();
if ($existingStatus) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_status')
->where('product_id', $m2ProductId)
->where('website_id', 0)
->where('stock_id', $stockStatusData['stock_id'])
->update($stockStatusData);
} else {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_status')
->insert($stockStatusData);
}
} catch (Exception $e) {
// Stock status table might not exist or have different structure, log and continue
Log::debug("Could not update stock status table: " . $e->getMessage());
}
} catch (Exception $e) {
Log::error("Error migrating stock for product M1 ID {$m1ProductId}, M2 ID {$m2ProductId}: " . $e->getMessage());
// Don't throw - just log the error and continue
}
}
/**
* Create a default stock item for a product
*/
protected function createDefaultStockItem($productId)
{
try {
// Check if stock item already exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->where('product_id', $productId)
->exists();
if (!$exists) {
// Create default stock item
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'cataloginventory_stock_item')
->insert([
'product_id' => $productId,
'stock_id' => 1,
'qty' => 0,
'is_in_stock' => 0,
'manage_stock' => 1,
'use_config_manage_stock' => 1,
'min_qty' => 0,
'min_sale_qty' => 1,
'max_sale_qty' => 0,
'is_qty_decimal' => 0,
'backorders' => 0,
'notify_stock_qty' => 0,
'use_config_notify_stock_qty' => 1,
'use_config_min_qty' => 1,
'use_config_min_sale_qty' => 1,
'use_config_max_sale_qty' => 1,
'use_config_backorders' => 1,
'use_config_enable_qty_inc' => 1,
'enable_qty_increments' => 0,
'use_config_qty_increments' => 1,
'qty_increments' => 0,
]);
}
} catch (Exception $e) {
Log::error("Error creating default stock item for product ID {$productId}: " . $e->getMessage());
}
}
/**
* Migrate all catalog_product_option tables from M1 to M2
*/
/**
* Migrate catalog_product_option tables from M1 to M2
*
* IMPORTANT: This method only performs INSERT and UPDATE operations.
* NO DATA IS DELETED. Existing product options in M2 will be preserved or updated.
*/
protected function migrateCatalogProductOptions()
{
$migratedCount = 0;
$errorCount = 0;
try {
// Build product ID mapping from M1 to M2
$productIdMapping = $this->buildProductIdMapping();
if (empty($productIdMapping)) {
Log::warning("No product ID mapping found for catalog_product_option migration");
return [
'migrated' => 0,
'errors' => 0
];
}
// Get all M1 product options
$m1Options = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option')
->get();
if ($m1Options->isEmpty()) {
return [
'migrated' => 0,
'errors' => 0
];
}
// Build option_id mapping (M1 option_id => M2 option_id)
$optionIdMapping = [];
// Migrate catalog_product_option table
foreach ($m1Options as $m1Option) {
try {
$m1ProductId = $m1Option->product_id;
$m1OptionId = $m1Option->option_id;
// Skip if product doesn't exist in M2
if (!isset($productIdMapping[$m1ProductId])) {
continue;
}
$m2ProductId = $productIdMapping[$m1ProductId];
// Check if option already exists in M2 (by product_id and type)
$existingOption = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option')
->where('product_id', $m2ProductId)
->where('type', $m1Option->type)
->where('sku', $m1Option->sku ?? '')
->first();
$optionData = [
'product_id' => $m2ProductId,
'type' => $m1Option->type,
'is_require' => $m1Option->is_require ?? 0,
'sku' => $m1Option->sku ?? null,
'max_characters' => $m1Option->max_characters ?? null,
'file_extension' => $m1Option->file_extension ?? null,
'image_size_x' => $m1Option->image_size_x ?? null,
'image_size_y' => $m1Option->image_size_y ?? null,
'sort_order' => $m1Option->sort_order ?? 0,
];
if ($existingOption) {
// Update existing option
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option')
->where('option_id', $existingOption->option_id)
->update($optionData);
$m2OptionId = $existingOption->option_id;
} else {
// Insert new option
$m2OptionId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option')
->insertGetId($optionData);
}
// Store option_id mapping
$optionIdMapping[$m1OptionId] = $m2OptionId;
$migratedCount++;
} catch (Exception $e) {
$errorCount++;
$errorMsg = $e->getMessage();
Log::error("Error migrating product option M1 ID {$m1OptionId}: " . $errorMsg);
$this->migrationLog[] = "ERROR: Failed to migrate product option M1 ID {$m1OptionId}: " . $errorMsg;
}
}
// Migrate catalog_product_option_price
$this->migrateCatalogProductOptionPrice($optionIdMapping);
// Migrate catalog_product_option_title
$this->migrateCatalogProductOptionTitle($optionIdMapping);
// Migrate catalog_product_option_type_value
$typeValueMapping = $this->migrateCatalogProductOptionTypeValue($optionIdMapping);
// Migrate catalog_product_option_type_price
$this->migrateCatalogProductOptionTypePrice($typeValueMapping);
// Migrate catalog_product_option_type_title
$this->migrateCatalogProductOptionTypeTitle($typeValueMapping);
} catch (Exception $e) {
$errorMsg = $e->getMessage();
Log::error("Error migrating catalog_product_option tables: " . $errorMsg);
$errorCount++;
$this->migrationLog[] = "ERROR: Failed to migrate catalog_product_option tables: " . $errorMsg;
}
return [
'migrated' => $migratedCount,
'errors' => $errorCount
];
}
/**
* Build product ID mapping from M1 to M2 (by SKU or entity_id)
*/
protected function buildProductIdMapping()
{
$mapping = [];
try {
$m1Products = $this->getMagento1Products();
$m2Products = $this->getMagento2Products();
// Build M2 product lookup by SKU and entity_id
$m2ProductBySku = [];
$m2ProductById = [];
foreach ($m2Products as $m2Product) {
$sku = $m2Product->sku ?? 'N/A';
if ($sku !== 'N/A' && !empty($sku)) {
$m2ProductBySku[$sku] = $m2Product->entity_id;
}
$m2ProductById[$m2Product->entity_id] = $m2Product->entity_id;
}
// Build mapping from M1 to M2
foreach ($m1Products as $m1Product) {
$m1ProductId = $m1Product->entity_id;
$m1Sku = $m1Product->sku ?? 'N/A';
$m2ProductId = null;
if ($m1Sku !== 'N/A' && !empty($m1Sku)) {
// Match by SKU
if (isset($m2ProductBySku[$m1Sku])) {
$m2ProductId = $m2ProductBySku[$m1Sku];
}
} else {
// Match by entity_id
if (isset($m2ProductById[$m1ProductId])) {
$m2ProductId = $m1ProductId;
}
}
if ($m2ProductId) {
$mapping[$m1ProductId] = $m2ProductId;
}
}
} catch (Exception $e) {
Log::error("Error building product ID mapping: " . $e->getMessage());
}
return $mapping;
}
/**
* Migrate catalog_product_option_price
*/
protected function migrateCatalogProductOptionPrice($optionIdMapping)
{
try {
foreach ($optionIdMapping as $m1OptionId => $m2OptionId) {
$m1Prices = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option_price')
->where('option_id', $m1OptionId)
->get();
foreach ($m1Prices as $m1Price) {
$priceData = [
'option_id' => $m2OptionId,
'store_id' => $m1Price->store_id ?? 0,
'price' => $m1Price->price ?? 0,
'price_type' => $m1Price->price_type ?? 'fixed',
];
// Check if exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_price')
->where('option_id', $m2OptionId)
->where('store_id', $priceData['store_id'])
->exists();
if ($exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_price')
->where('option_id', $m2OptionId)
->where('store_id', $priceData['store_id'])
->update($priceData);
} else {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_price')
->insert($priceData);
}
}
}
} catch (Exception $e) {
Log::error("Error migrating catalog_product_option_price: " . $e->getMessage());
}
}
/**
* Migrate catalog_product_option_title
*/
protected function migrateCatalogProductOptionTitle($optionIdMapping)
{
try {
foreach ($optionIdMapping as $m1OptionId => $m2OptionId) {
$m1Titles = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option_title')
->where('option_id', $m1OptionId)
->get();
foreach ($m1Titles as $m1Title) {
$titleData = [
'option_id' => $m2OptionId,
'store_id' => $m1Title->store_id ?? 0,
'title' => $m1Title->title ?? '',
];
// Check if exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_title')
->where('option_id', $m2OptionId)
->where('store_id', $titleData['store_id'])
->exists();
if ($exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_title')
->where('option_id', $m2OptionId)
->where('store_id', $titleData['store_id'])
->update($titleData);
} else {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_title')
->insert($titleData);
}
}
}
} catch (Exception $e) {
Log::error("Error migrating catalog_product_option_title: " . $e->getMessage());
}
}
/**
* Migrate catalog_product_option_type_value and return type_value_id mapping
*/
protected function migrateCatalogProductOptionTypeValue($optionIdMapping)
{
$typeValueMapping = [];
try {
foreach ($optionIdMapping as $m1OptionId => $m2OptionId) {
$m1TypeValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option_type_value')
->where('option_id', $m1OptionId)
->get();
foreach ($m1TypeValues as $m1TypeValue) {
$m1TypeValueId = $m1TypeValue->option_type_id;
$typeValueData = [
'option_id' => $m2OptionId,
'sku' => $m1TypeValue->sku ?? null,
'sort_order' => $m1TypeValue->sort_order ?? 0,
];
// Check if exists (by option_id and sku/sort_order)
$existing = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_value')
->where('option_id', $m2OptionId)
->where('sku', $typeValueData['sku'])
->where('sort_order', $typeValueData['sort_order'])
->first();
if ($existing) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_value')
->where('option_type_id', $existing->option_type_id)
->update($typeValueData);
$m2TypeValueId = $existing->option_type_id;
} else {
$m2TypeValueId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_value')
->insertGetId($typeValueData);
}
$typeValueMapping[$m1TypeValueId] = $m2TypeValueId;
}
}
} catch (Exception $e) {
Log::error("Error migrating catalog_product_option_type_value: " . $e->getMessage());
}
return $typeValueMapping;
}
/**
* Migrate catalog_product_option_type_price
*/
protected function migrateCatalogProductOptionTypePrice($typeValueMapping)
{
try {
foreach ($typeValueMapping as $m1TypeValueId => $m2TypeValueId) {
$m1Prices = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option_type_price')
->where('option_type_id', $m1TypeValueId)
->get();
foreach ($m1Prices as $m1Price) {
$priceData = [
'option_type_id' => $m2TypeValueId,
'store_id' => $m1Price->store_id ?? 0,
'price' => $m1Price->price ?? 0,
'price_type' => $m1Price->price_type ?? 'fixed',
];
// Check if exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_price')
->where('option_type_id', $m2TypeValueId)
->where('store_id', $priceData['store_id'])
->exists();
if ($exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_price')
->where('option_type_id', $m2TypeValueId)
->where('store_id', $priceData['store_id'])
->update($priceData);
} else {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_price')
->insert($priceData);
}
}
}
} catch (Exception $e) {
Log::error("Error migrating catalog_product_option_type_price: " . $e->getMessage());
}
}
/**
* Migrate catalog_product_option_type_title
*/
protected function migrateCatalogProductOptionTypeTitle($typeValueMapping)
{
try {
foreach ($typeValueMapping as $m1TypeValueId => $m2TypeValueId) {
$m1Titles = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_option_type_title')
->where('option_type_id', $m1TypeValueId)
->get();
foreach ($m1Titles as $m1Title) {
$titleData = [
'option_type_id' => $m2TypeValueId,
'store_id' => $m1Title->store_id ?? 0,
'title' => $m1Title->title ?? '',
];
// Check if exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_title')
->where('option_type_id', $m2TypeValueId)
->where('store_id', $titleData['store_id'])
->exists();
if ($exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_title')
->where('option_type_id', $m2TypeValueId)
->where('store_id', $titleData['store_id'])
->update($titleData);
} else {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_option_type_title')
->insert($titleData);
}
}
}
} catch (Exception $e) {
Log::error("Error migrating catalog_product_option_type_title: " . $e->getMessage());
}
}
/**
* Ensure product is enabled and visible (required for products to appear in categories after reindex)
*/
protected function ensureProductIsEnabledAndVisible($productId)
{
try {
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return;
}
// Get attribute IDs
$statusAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'status')
->value('attribute_id');
$visibilityAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'visibility')
->value('attribute_id');
// Set status to 1 (enabled) if not already set
if ($statusAttributeId) {
$statusTable = $this->magento2Prefix . 'catalog_product_entity_int';
$hasStatus = DB::connection($this->magento2Connection)
->table($statusTable)
->where('entity_id', $productId)
->where('attribute_id', $statusAttributeId)
->where('store_id', 0)
->exists();
if (!$hasStatus) {
DB::connection($this->magento2Connection)
->table($statusTable)
->insert([
'attribute_id' => $statusAttributeId,
'store_id' => 0,
'entity_id' => $productId,
'value' => 1, // Enabled
]);
} else {
// Update to enabled if it's disabled
DB::connection($this->magento2Connection)
->table($statusTable)
->where('entity_id', $productId)
->where('attribute_id', $statusAttributeId)
->where('store_id', 0)
->update(['value' => 1]);
}
}
// Set visibility to 4 (Catalog, Search) if not already set
if ($visibilityAttributeId) {
$visibilityTable = $this->magento2Prefix . 'catalog_product_entity_int';
$hasVisibility = DB::connection($this->magento2Connection)
->table($visibilityTable)
->where('entity_id', $productId)
->where('attribute_id', $visibilityAttributeId)
->where('store_id', 0)
->exists();
if (!$hasVisibility) {
DB::connection($this->magento2Connection)
->table($visibilityTable)
->insert([
'attribute_id' => $visibilityAttributeId,
'store_id' => 0,
'entity_id' => $productId,
'value' => 4, // Catalog, Search
]);
} else {
// Update to Catalog, Search if it's Not Visible or Search Only
$currentVisibility = DB::connection($this->magento2Connection)
->table($visibilityTable)
->where('entity_id', $productId)
->where('attribute_id', $visibilityAttributeId)
->where('store_id', 0)
->value('value');
// Only update if visibility is 1 (Not Visible) or 2 (Catalog)
// 3 = Search, 4 = Catalog, Search (both)
if ($currentVisibility == 1 || $currentVisibility == 2) {
DB::connection($this->magento2Connection)
->table($visibilityTable)
->where('entity_id', $productId)
->where('attribute_id', $visibilityAttributeId)
->where('store_id', 0)
->update(['value' => 4]);
}
}
}
} catch (Exception $e) {
Log::warning("Error ensuring product {$productId} is enabled and visible: " . $e->getMessage());
}
}
/**
* Get Magento 1 category tree with products
*/
public function getMagento1CategoryTreeWithProducts()
{
try {
$categories = $this->getMagento1Categories();
$categoryMap = [];
// Build category map
foreach ($categories as $category) {
$categoryMap[$category->entity_id] = [
'id' => $category->entity_id,
'name' => $category->name,
'parent_id' => $category->parent_id,
'is_active' => $category->is_active ?? true,
'path' => $category->path ?? '',
'children' => [],
'products' => []
];
}
// Get all products in categories
$categoryProducts = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_category_product')
->select('category_id', 'product_id', 'position')
->orderBy('category_id')
->orderBy('position')
->get();
// Get product details
$productIds = $categoryProducts->pluck('product_id')->unique()->toArray();
$products = [];
if (!empty($productIds)) {
// Check if SKU column exists in entity table
$hasSkuColumn = false;
try {
$testQuery = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->select('sku')
->limit(1)
->first();
$hasSkuColumn = true;
} catch (Exception $e) {
$hasSkuColumn = false;
}
$entityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if ($entityTypeId) {
// Get SKU attribute ID (for EAV storage)
$skuAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'sku')
->value('attribute_id');
// Get name attribute ID
$nameAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
// Get SKUs - from entity table or EAV
$skus = [];
if ($hasSkuColumn) {
// SKU is stored in entity table
$skuValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity')
->whereIn('entity_id', $productIds)
->pluck('sku', 'entity_id')
->toArray();
$skus = $skuValues;
} else if ($skuAttributeId) {
// SKU is stored in EAV table
$skuValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $skuAttributeId)
->where('store_id', 0)
->whereIn('entity_id', $productIds)
->pluck('value', 'entity_id')
->toArray();
$skus = $skuValues;
}
// Get names
$names = [];
if ($nameAttributeId) {
$nameValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->whereIn('entity_id', $productIds)
->pluck('value', 'entity_id')
->toArray();
$names = $nameValues;
}
// Build products array
foreach ($productIds as $productId) {
$sku = $skus[$productId] ?? 'N/A';
$products[$productId] = [
'id' => $productId,
'sku' => !empty($sku) ? $sku : 'N/A',
'name' => $names[$productId] ?? 'Unnamed Product'
];
}
}
}
// Assign products to categories
foreach ($categoryProducts as $cp) {
if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) {
$categoryMap[$cp->category_id]['products'][] = [
'id' => $products[$cp->product_id]['id'],
'sku' => $products[$cp->product_id]['sku'],
'name' => $products[$cp->product_id]['name'],
'position' => $cp->position ?? 0
];
}
}
// Build tree structure
$tree = [];
foreach ($categoryMap as $categoryId => $category) {
if ($category['parent_id'] == 0 || $category['parent_id'] == 1) {
// Root category
$tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap);
}
}
return $tree;
} catch (Exception $e) {
Log::error('Error fetching M1 category tree with products: ' . $e->getMessage());
return [];
}
}
/**
* Get Magento 2 category tree with products
*/
public function getMagento2CategoryTreeWithProducts()
{
try {
$categories = $this->getMagento2Categories();
$categoryMap = [];
// Build category map
foreach ($categories as $category) {
$categoryMap[$category->entity_id] = [
'id' => $category->entity_id,
'name' => $category->name,
'parent_id' => $category->parent_id,
'is_active' => $category->is_active ?? true,
'path' => $category->path ?? '',
'children' => [],
'products' => []
];
}
// Get all products in categories
$categoryProducts = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->select('category_id', 'product_id', 'position')
->orderBy('category_id')
->orderBy('position')
->get();
// Get product details
$productIds = $categoryProducts->pluck('product_id')->unique()->toArray();
$products = [];
if (!empty($productIds)) {
// Check if SKU column exists in entity table
$hasSkuColumn = false;
try {
$testQuery = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->select('sku')
->limit(1)
->first();
$hasSkuColumn = true;
} catch (Exception $e) {
$hasSkuColumn = false;
}
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if ($entityTypeId) {
// Get SKU attribute ID (for EAV storage)
$skuAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'sku')
->value('attribute_id');
// Get name attribute ID
$nameAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
// Get SKUs - from entity table or EAV
$skus = [];
if ($hasSkuColumn) {
// SKU is stored in entity table
$skuValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->whereIn('entity_id', $productIds)
->pluck('sku', 'entity_id')
->toArray();
$skus = $skuValues;
} else if ($skuAttributeId) {
// SKU is stored in EAV table
$skuValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $skuAttributeId)
->where('store_id', 0)
->whereIn('entity_id', $productIds)
->pluck('value', 'entity_id')
->toArray();
$skus = $skuValues;
}
// Get names
$names = [];
if ($nameAttributeId) {
$nameValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->whereIn('entity_id', $productIds)
->pluck('value', 'entity_id')
->toArray();
$names = $nameValues;
}
// Build products array
foreach ($productIds as $productId) {
$sku = $skus[$productId] ?? 'N/A';
$products[$productId] = [
'id' => $productId,
'sku' => !empty($sku) ? $sku : 'N/A',
'name' => $names[$productId] ?? 'Unnamed Product'
];
}
}
}
// Assign products to categories
foreach ($categoryProducts as $cp) {
if (isset($categoryMap[$cp->category_id]) && isset($products[$cp->product_id])) {
$categoryMap[$cp->category_id]['products'][] = [
'id' => $products[$cp->product_id]['id'],
'sku' => $products[$cp->product_id]['sku'],
'name' => $products[$cp->product_id]['name'],
'position' => $cp->position ?? 0
];
}
}
// Build tree structure
$tree = [];
foreach ($categoryMap as $categoryId => $category) {
if ($category['parent_id'] == 0 || $category['parent_id'] == 1) {
// Root category
$tree[] = $this->buildCategoryTreeWithProducts($category, $categoryMap);
}
}
return $tree;
} catch (Exception $e) {
Log::error('Error fetching M2 category tree with products: ' . $e->getMessage());
return [];
}
}
/**
* Build category tree structure with products recursively
*/
protected function buildCategoryTreeWithProducts($category, $categoryMap)
{
$node = [
'id' => $category['id'],
'name' => $category['name'],
'is_active' => $category['is_active'],
'products' => $category['products'],
'children' => []
];
// Find children
foreach ($categoryMap as $catId => $cat) {
if ($cat['parent_id'] == $category['id']) {
$node['children'][] = $this->buildCategoryTreeWithProducts($cat, $categoryMap);
}
}
return $node;
}
/**
* Get category name by ID
*/
protected function getCategoryNameById($categoryId, $source = 'm1')
{
try {
$connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection;
$prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix;
$entityTypeId = DB::connection($connection)
->table($prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_category')
->value('entity_type_id');
if (!$entityTypeId) {
return null;
}
$nameAttributeId = DB::connection($connection)
->table($prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
if (!$nameAttributeId) {
return null;
}
$name = DB::connection($connection)
->table($prefix . 'catalog_category_entity_varchar')
->where('entity_id', $categoryId)
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->value('value');
return $name;
} catch (Exception $e) {
Log::warning("Error getting category name for ID {$categoryId}: " . $e->getMessage());
return null;
}
}
/**
* Find category by name (case-insensitive, matches any parent)
*/
protected function findCategoryByName($categoryName, $source = 'm2')
{
try {
$connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection;
$prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix;
$entityTypeId = DB::connection($connection)
->table($prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_category')
->value('entity_type_id');
if (!$entityTypeId) {
return null;
}
$nameAttributeId = DB::connection($connection)
->table($prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'name')
->value('attribute_id');
if (!$nameAttributeId) {
return null;
}
// Find category by name (case-insensitive)
$category = DB::connection($connection)
->table($prefix . 'catalog_category_entity_varchar')
->where('attribute_id', $nameAttributeId)
->where('store_id', 0)
->whereRaw('LOWER(value) = ?', [strtolower(trim($categoryName))])
->first();
return $category ? $category->entity_id : null;
} catch (Exception $e) {
Log::warning("Error finding category by name '{$categoryName}': " . $e->getMessage());
return null;
}
}
/**
* Recursively delete all children of a category
*/
protected function deleteCategoryChildren($categoryId, $connection, $prefix)
{
// Get all children
$children = DB::connection($connection)
->table($prefix . 'catalog_category_entity')
->where('parent_id', $categoryId)
->get();
// Recursively delete each child
foreach ($children as $child) {
$this->deleteCategoryChildren($child->entity_id, $connection, $prefix);
$this->deleteCategoryData($child->entity_id, $connection, $prefix);
}
}
/**
* Fix category products - ensure products are added to categories if missing from catalog_category_product table
* Only looks at M2 products and their category_ids attribute
*/
public function fixCategoryProducts()
{
try {
$this->migrationLog = [];
$addedCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Get entity type ID for catalog_product
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeId) {
return [
'success' => false,
'message' => 'Could not find catalog_product entity type in Magento 2',
'added' => 0,
'skipped' => 0,
'errors' => 0,
'log' => []
];
}
// Get category_ids attribute ID
$categoryIdsAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'category_ids')
->value('attribute_id');
// Get all M2 products
$m2Products = $this->getMagento2Products();
// Get all products that are already in catalog_category_product table
$productsInCategories = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->distinct()
->pluck('product_id')
->toArray();
// Get all valid M2 category IDs
$validCategoryIds = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_entity')
->pluck('entity_id')
->toArray();
DB::connection($this->magento2Connection)->beginTransaction();
foreach ($m2Products as $m2Product) {
try {
$m2ProductId = $m2Product->entity_id;
$m2Sku = $m2Product->sku ?? 'N/A';
// Check if product is already in catalog_category_product table
if (in_array($m2ProductId, $productsInCategories)) {
$skippedCount++;
continue;
}
// Get category_ids from product attribute
$categoryIdsString = null;
if ($categoryIdsAttributeId) {
// Try to get from varchar table first
$categoryIdsValue = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_varchar')
->where('entity_id', $m2ProductId)
->where('attribute_id', $categoryIdsAttributeId)
->where('store_id', 0)
->value('value');
if ($categoryIdsValue) {
$categoryIdsString = $categoryIdsValue;
} else {
// Try text table
$categoryIdsValue = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_text')
->where('entity_id', $m2ProductId)
->where('attribute_id', $categoryIdsAttributeId)
->where('store_id', 0)
->value('value');
if ($categoryIdsValue) {
$categoryIdsString = $categoryIdsValue;
}
}
}
if (empty($categoryIdsString)) {
$skippedCount++;
$this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - no category_ids attribute found";
continue;
}
// Parse category IDs (comma-separated string)
$categoryIds = array_filter(
array_map('trim', explode(',', $categoryIdsString)),
function($id) use ($validCategoryIds) {
return !empty($id) && is_numeric($id) && in_array((int)$id, $validCategoryIds);
}
);
if (empty($categoryIds)) {
$skippedCount++;
$this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - no valid category IDs found in attribute";
continue;
}
// Ensure product is enabled and visible
$this->ensureProductIsEnabledAndVisible($m2ProductId);
// Add product to categories
$categoriesAdded = 0;
$invalidCategories = [];
foreach ($categoryIds as $categoryId) {
$categoryId = (int)$categoryId;
// Check if association already exists
$exists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->where('category_id', $categoryId)
->where('product_id', $m2ProductId)
->exists();
if (!$exists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_category_product')
->insert([
'category_id' => $categoryId,
'product_id' => $m2ProductId,
'position' => 0, // Default position
]);
$categoriesAdded++;
}
}
if ($categoriesAdded > 0) {
$addedCount++;
$invalidMsg = !empty($invalidCategories) ? " (invalid categories: " . implode(', ', $invalidCategories) . ")" : "";
$this->migrationLog[] = "ADDED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - added to {$categoriesAdded} category(ies){$invalidMsg}";
} else {
$skippedCount++;
$invalidMsg = !empty($invalidCategories) ? " (invalid categories: " . implode(', ', $invalidCategories) . ")" : "";
$this->migrationLog[] = "SKIPPED: M2 Product ID {$m2ProductId} (SKU: {$m2Sku}) - categories already exist or invalid{$invalidMsg}";
}
} catch (Exception $e) {
$errorCount++;
$m2Sku = $m2Product->sku ?? 'N/A';
$this->migrationLog[] = "ERROR: Failed to fix categories for M2 Product ID {$m2ProductId} (SKU: {$m2Sku}): " . $e->getMessage();
Log::error("Error fixing category products for M2 Product ID {$m2ProductId}: " . $e->getMessage());
}
}
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => "Fixed category products. Added: {$addedCount}, Skipped: {$skippedCount}, Errors: {$errorCount}",
'added' => $addedCount,
'skipped' => $skippedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
} catch (Exception $e) {
if (isset($this->magento2Connection)) {
DB::connection($this->magento2Connection)->rollBack();
}
Log::error('Error fixing category products: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to fix category products: ' . $e->getMessage(),
'added' => 0,
'skipped' => 0,
'errors' => 0,
'log' => []
];
}
}
/**
* Delete category data (attributes and entity)
*/
protected function deleteCategoryData($categoryId, $connection, $prefix)
{
// Get entity type ID
$entityTypeId = DB::connection($connection)
->table($prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_category')
->value('entity_type_id');
if ($entityTypeId) {
// Delete attribute values from all attribute tables
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$table = $prefix . 'catalog_category_entity_' . $tableType;
try {
DB::connection($connection)
->table($table)
->where('entity_id', $categoryId)
->delete();
} catch (Exception $e) {
// Table might not exist, continue
Log::warning("Table {$table} might not exist or has no data: " . $e->getMessage());
}
}
// Delete category product associations
try {
DB::connection($connection)
->table($prefix . 'catalog_category_product')
->where('category_id', $categoryId)
->delete();
} catch (Exception $e) {
Log::warning("Table {$prefix}catalog_category_product might not exist: " . $e->getMessage());
}
}
// Delete the category entity
DB::connection($connection)
->table($prefix . 'catalog_category_entity')
->where('entity_id', $categoryId)
->delete();
}
/**
* Get all customers from Magento 1
*/
public function getMagento1Customers()
{
try {
// Get entity type ID for customer
$entityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if (!$entityTypeId) {
Log::warning('Magento 1 customer entity type not found');
return collect([]);
}
Log::info("Magento 1 customer entity_type_id: {$entityTypeId}");
// Get base customer data - email is stored directly in customer_entity table
$customers = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'customer_entity')
->select('entity_id', 'email', 'website_id', 'group_id', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
Log::info("Found " . $customers->count() . " customers in Magento 1 customer_entity table");
// Get firstname and lastname attribute IDs (these are in EAV)
$firstnameAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'firstname')
->value('attribute_id');
$lastnameAttributeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'lastname')
->value('attribute_id');
// Get firstname and lastname from EAV
$firstnames = [];
$lastnames = [];
if ($firstnameAttributeId) {
$firstnameValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'customer_entity_varchar')
->where('attribute_id', $firstnameAttributeId)
->pluck('value', 'entity_id')
->toArray();
$firstnames = $firstnameValues;
}
if ($lastnameAttributeId) {
$lastnameValues = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'customer_entity_varchar')
->where('attribute_id', $lastnameAttributeId)
->pluck('value', 'entity_id')
->toArray();
$lastnames = $lastnameValues;
}
// Combine data - email is already in customer object from the query
foreach ($customers as $customer) {
// Email is already in customer object, just normalize it
$customer->email = !empty($customer->email) ? trim($customer->email) : null;
$customer->firstname = $firstnames[$customer->entity_id] ?? 'N/A';
$customer->lastname = $lastnames[$customer->entity_id] ?? 'N/A';
}
Log::info("Returning " . $customers->count() . " Magento 1 customers");
return $customers;
} catch (Exception $e) {
Log::error('Error fetching Magento 1 customers: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return collect([]);
}
}
/**
* Get all customers from Magento 2
*/
public function getMagento2Customers()
{
try {
// Get entity type ID for customer
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if (!$entityTypeId) {
Log::warning('Magento 2 customer entity type not found');
return collect([]);
}
Log::info("Magento 2 customer entity_type_id: {$entityTypeId}");
// Get base customer data - email is stored directly in customer_entity table
$customers = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->select('entity_id', 'email', 'website_id', 'group_id', 'created_at', 'updated_at')
->orderBy('entity_id')
->get();
Log::info("Found " . $customers->count() . " customers in Magento 2 customer_entity table");
// Get firstname and lastname attribute IDs (these are in EAV)
$firstnameAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'firstname')
->value('attribute_id');
$lastnameAttributeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->where('attribute_code', 'lastname')
->value('attribute_id');
// Get firstname and lastname from EAV
$firstnames = [];
$lastnames = [];
if ($firstnameAttributeId) {
$firstnameValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity_varchar')
->where('attribute_id', $firstnameAttributeId)
->pluck('value', 'entity_id')
->toArray();
$firstnames = $firstnameValues;
}
if ($lastnameAttributeId) {
$lastnameValues = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity_varchar')
->where('attribute_id', $lastnameAttributeId)
->pluck('value', 'entity_id')
->toArray();
$lastnames = $lastnameValues;
}
// Combine data - email is already in customer object from the query
foreach ($customers as $customer) {
// Email is already in customer object, just normalize it
$customer->email = !empty($customer->email) ? trim($customer->email) : null;
$customer->firstname = $firstnames[$customer->entity_id] ?? 'N/A';
$customer->lastname = $lastnames[$customer->entity_id] ?? 'N/A';
}
Log::info("Returning " . $customers->count() . " Magento 2 customers");
return $customers;
} catch (Exception $e) {
Log::error('Error fetching Magento 2 customers: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return collect([]);
}
}
/**
* Get Magento 1 customers that don't exist in Magento 2
*/
public function getM1CustomersNotInM2()
{
try {
$m1Customers = $this->getMagento1Customers();
$m2Customers = $this->getMagento2Customers();
Log::info("M1 Customers Not In M2: M1 count = " . $m1Customers->count() . ", M2 count = " . $m2Customers->count());
// Get all M2 emails - create a set for faster lookup
$m2EmailsSet = [];
foreach ($m2Customers as $m2Customer) {
$email = $m2Customer->email ?? null;
if (!empty($email) && is_string($email)) {
$normalizedEmail = strtolower(trim($email));
if (!empty($normalizedEmail)) {
$m2EmailsSet[$normalizedEmail] = true;
}
}
}
Log::info("M1 Customers Not In M2: M2 emails count = " . count($m2EmailsSet));
// Get all M1 emails for comparison
$m1EmailsWithCustomers = [];
foreach ($m1Customers as $m1Customer) {
$email = $m1Customer->email ?? null;
if (!empty($email) && is_string($email)) {
$normalizedEmail = strtolower(trim($email));
if (!empty($normalizedEmail)) {
$m1EmailsWithCustomers[$normalizedEmail] = $m1Customer;
}
}
}
Log::info("M1 Customers Not In M2: M1 emails count = " . count($m1EmailsWithCustomers));
// Filter M1 customers that don't exist in M2
$missingCustomers = collect();
foreach ($m1EmailsWithCustomers as $normalizedEmail => $m1Customer) {
if (!isset($m2EmailsSet[$normalizedEmail])) {
$missingCustomers->push($m1Customer);
}
}
Log::info("M1 Customers Not In M2: Missing customers count = " . $missingCustomers->count());
return $missingCustomers->values();
} catch (Exception $e) {
Log::error('Error fetching missing customers: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return collect([]);
}
}
/**
* Get Magento 2 customers that don't exist in Magento 1
*/
public function getM2CustomersNotInM1()
{
try {
$m1Customers = $this->getMagento1Customers();
$m2Customers = $this->getMagento2Customers();
// Get all M1 emails - create a set for faster lookup
$m1EmailsSet = [];
foreach ($m1Customers as $m1Customer) {
$email = $m1Customer->email ?? null;
if (!empty($email) && is_string($email)) {
$normalizedEmail = strtolower(trim($email));
if (!empty($normalizedEmail)) {
$m1EmailsSet[$normalizedEmail] = true;
}
}
}
// Filter M2 customers that don't exist in M1
$missingCustomers = collect();
foreach ($m2Customers as $m2Customer) {
$email = $m2Customer->email ?? null;
if (!empty($email) && is_string($email)) {
$normalizedEmail = strtolower(trim($email));
if (!empty($normalizedEmail) && !isset($m1EmailsSet[$normalizedEmail])) {
$missingCustomers->push($m2Customer);
}
}
}
return $missingCustomers->values();
} catch (Exception $e) {
Log::error('Error fetching M2 customers not in M1: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return collect([]);
}
}
/**
* Migrate all customers from Magento 1 to Magento 2
*/
public function migrateCustomers($dryRun = false)
{
try {
$this->migrationLog = [];
$addedCount = 0;
$updatedCount = 0;
$errorCount = 0;
// Get M1 and M2 entity type IDs
$m1EntityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if (!$m1EntityTypeId || !$m2EntityTypeId) {
return [
'success' => false,
'message' => 'Entity type not found',
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => []
];
}
// Get all M1 customers
$m1Customers = $this->getMagento1Customers();
// Get all customer attribute IDs from M1
$m1AttributeIds = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $m1EntityTypeId)
->pluck('attribute_id', 'attribute_code')
->toArray();
// Get all customer attribute IDs from M2
$m2AttributeIds = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $m2EntityTypeId)
->pluck('attribute_id', 'attribute_code')
->toArray();
if (!$dryRun) {
DB::connection($this->magento2Connection)->beginTransaction();
}
foreach ($m1Customers as $m1Customer) {
try {
$m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null;
if (empty($m1Email)) {
$this->migrationLog[] = "SKIPPED: Customer ID {$m1Customer->entity_id} - no email address";
continue;
}
// Check if customer exists in M2 by email (case-insensitive)
$m2Customer = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->whereRaw('LOWER(TRIM(email)) = ?', [strtolower(trim($m1Email))])
->first();
$m2CustomerId = null;
$isNew = false;
if ($m2Customer) {
// Customer exists, update
$m2CustomerId = $m2Customer->entity_id;
if (!$dryRun) {
$this->migrationLog[] = "Updating existing customer: {$m1Email} (ID: {$m2CustomerId})";
} else {
$this->migrationLog[] = "Would update existing customer: {$m1Email} (ID: {$m2CustomerId})";
}
$updatedCount++;
} else {
// Customer doesn't exist, create
if (!$dryRun) {
// Insert customer entity
$m2CustomerId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->insertGetId([
'email' => $m1Email,
'website_id' => $m1Customer->website_id ?? 1,
'group_id' => $m1Customer->group_id ?? 1,
'created_at' => $m1Customer->created_at ?? now(),
'updated_at' => $m1Customer->updated_at ?? now(),
]);
$this->migrationLog[] = "Added new customer: {$m1Email} (ID: {$m2CustomerId})";
} else {
$this->migrationLog[] = "Would add new customer: {$m1Email}";
$m2CustomerId = 0; // Placeholder for dry run
}
$addedCount++;
$isNew = true;
}
if (!$dryRun && $m2CustomerId) {
// Migrate customer attributes
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$m1Table = $this->magento1Prefix . 'customer_entity_' . $tableType;
$m2Table = $this->magento2Prefix . 'customer_entity_' . $tableType;
try {
// Get all attributes for this customer from M1
$m1Attributes = DB::connection($this->magento1Connection)
->table($m1Table)
->where('entity_id', $m1Customer->entity_id)
->get();
foreach ($m1Attributes as $m1Attr) {
// Check if attribute exists in M2
$attributeCode = array_search($m1Attr->attribute_id, $m1AttributeIds);
if ($attributeCode && isset($m2AttributeIds[$attributeCode])) {
$m2AttributeId = $m2AttributeIds[$attributeCode];
// Check if attribute value already exists (customer attributes don't have store_id)
$exists = DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2CustomerId)
->where('attribute_id', $m2AttributeId)
->exists();
if (!$exists) {
// Customer attributes don't have store_id column
$insertData = [
'entity_id' => $m2CustomerId,
'attribute_id' => $m2AttributeId,
'value' => $m1Attr->value ?? null,
];
DB::connection($this->magento2Connection)
->table($m2Table)
->insert($insertData);
} else {
// Update existing attribute
DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2CustomerId)
->where('attribute_id', $m2AttributeId)
->update(['value' => $m1Attr->value ?? null]);
}
}
}
} catch (Exception $e) {
// Table might not exist, continue
Log::warning("Table {$m1Table} or {$m2Table} might not exist: " . $e->getMessage());
}
}
}
} catch (Exception $e) {
$errorCount++;
$m1Email = $m1Customer->email ?? 'N/A';
$this->migrationLog[] = "ERROR: Failed to migrate customer {$m1Email}: " . $e->getMessage();
Log::error("Error migrating customer {$m1Email}: " . $e->getMessage());
}
}
if (!$dryRun) {
DB::connection($this->magento2Connection)->commit();
}
return [
'success' => true,
'message' => $dryRun ? 'Dry run completed' : 'Customer migration completed',
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
} catch (Exception $e) {
if (!$dryRun) {
DB::connection($this->magento2Connection)->rollBack();
}
Log::error('Error migrating customers: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage(),
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => []
];
}
}
/**
* Delete a single customer from Magento 2
*/
public function deleteM2Customer($customerId)
{
try {
DB::connection($this->magento2Connection)->beginTransaction();
// Get entity type ID
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if ($entityTypeId) {
// Delete attribute values from all attribute tables
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$table = $this->magento2Prefix . 'customer_entity_' . $tableType;
try {
DB::connection($this->magento2Connection)
->table($table)
->where('entity_id', $customerId)
->delete();
} catch (Exception $e) {
Log::warning("Table {$table} might not exist: " . $e->getMessage());
}
}
}
// Delete the customer entity
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->where('entity_id', $customerId)
->delete();
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => 'Customer deleted successfully'
];
} catch (Exception $e) {
DB::connection($this->magento2Connection)->rollBack();
Log::error("Error deleting customer {$customerId}: " . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to delete customer: ' . $e->getMessage()
];
}
}
/**
* Migrate product options from M1 mageworx_custom_option_ tables to M2 mageworx_optiontemplates_ tables
*/
public function migrateProductOptions($dryRun = false)
{
$this->migrationLog = [];
$migratedCount = 0;
$skippedCount = 0;
$errorCount = 0;
try {
// Get all mageworx custom option tables from M1
$m1Tables = $this->getMageworxM1Tables();
if (empty($m1Tables)) {
return [
'success' => false,
'message' => 'No mageworx_custom_option_ tables found in Magento 1',
'migrated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => []
];
}
$this->migrationLog[] = "Found " . count($m1Tables) . " mageworx custom option tables in M1";
// Get available M2 tables for reference
$m2Tables = $this->getMageworxM2Tables();
$this->migrationLog[] = "Found " . count($m2Tables) . " mageworx option templates tables in M2";
// Map M1 table names to M2 table names
$tableMapping = $this->getMageworxTableMapping();
// Sort tables by dependency order (parent tables first)
$sortedTables = $this->sortTablesByDependency($m1Tables, $tableMapping);
$this->migrationLog[] = "Migrating tables in dependency order";
foreach ($sortedTables as $m1Table) {
try {
// Get the corresponding M2 table name
$m2Table = $this->getM2TableName($m1Table, $tableMapping);
if (!$m2Table) {
$this->migrationLog[] = "INFO: Skipping M1 table {$m1Table} - no corresponding M2 table exists or mapping not configured";
$skippedCount++;
continue;
}
// Check if M2 table exists
if (!$this->tableExists($this->magento2Connection, $this->magento2Prefix . $m2Table)) {
$this->migrationLog[] = "WARNING: M2 table does not exist: {$m2Table} (mapped from M1: {$m1Table}). Available M2 tables: " . implode(', ', array_slice($m2Tables, 0, 10));
$skippedCount++;
continue;
}
// Get all data from M1 table
$m1Data = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . $m1Table)
->get();
if ($m1Data->isEmpty()) {
$this->migrationLog[] = "INFO: No data in M1 table: {$m1Table}";
continue;
}
$this->migrationLog[] = "Migrating {$m1Data->count()} records from {$m1Table} to {$m2Table}";
if (!$dryRun) {
DB::connection($this->magento2Connection)->beginTransaction();
try {
foreach ($m1Data as $row) {
$rowArray = (array) $row;
// Map column names if needed
$mappedRow = $this->mapMageworxColumns($m1Table, $m2Table, $rowArray);
// Validate foreign key constraints
$fkValid = $this->validateForeignKeyConstraints($m2Table, $mappedRow);
if (!$fkValid) {
$skippedCount++;
continue;
}
// Check if record already exists in M2 (by primary key or unique identifier)
$exists = $this->recordExists($m2Table, $mappedRow);
if ($exists) {
// Update existing record
$this->updateM2Record($m2Table, $mappedRow);
$this->migrationLog[] = "Updated record in {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")";
} else {
// Insert new record
$this->insertM2Record($m2Table, $mappedRow);
$this->migrationLog[] = "Inserted record into {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")";
}
$migratedCount++;
}
DB::connection($this->magento2Connection)->commit();
} catch (Exception $e) {
DB::connection($this->magento2Connection)->rollBack();
throw $e;
}
} else {
// Dry run - just log what would be migrated
foreach ($m1Data as $row) {
$rowArray = (array) $row;
$mappedRow = $this->mapMageworxColumns($m1Table, $m2Table, $rowArray);
// Validate foreign key constraints in dry run
$fkValid = $this->validateForeignKeyConstraints($m2Table, $mappedRow, true);
if (!$fkValid) {
$skippedCount++;
continue;
}
$exists = $this->recordExists($m2Table, $mappedRow);
if ($exists) {
$this->migrationLog[] = "Would update record in {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")";
} else {
$this->migrationLog[] = "Would insert record into {$m2Table} (ID: " . ($mappedRow['id'] ?? $mappedRow['option_id'] ?? 'N/A') . ")";
}
$migratedCount++;
}
}
} catch (Exception $e) {
$errorCount++;
$this->migrationLog[] = "ERROR migrating {$m1Table}: " . $e->getMessage();
Log::error("Error migrating mageworx table {$m1Table}: " . $e->getMessage());
}
}
return [
'success' => true,
'message' => $dryRun ? 'Dry run completed' : 'Product options migration completed',
'migrated' => $migratedCount,
'skipped' => $skippedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
} catch (Exception $e) {
Log::error('Product options migration error: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage(),
'migrated' => $migratedCount,
'skipped' => $skippedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
}
}
/**
* Get all mageworx custom option tables from M1
*/
protected function getMageworxM1Tables()
{
try {
$tables = [];
$database = config("database.connections.{$this->magento1Connection}.database");
$prefix = $this->magento1Prefix;
// Get all tables from information_schema - check for both singular and plural forms
$allTables = DB::connection($this->magento1Connection)
->select("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND (TABLE_NAME LIKE ? OR TABLE_NAME LIKE ?)", [
$database,
$prefix . 'mageworx_custom_option_%',
$prefix . 'mageworx_custom_options_%'
]);
foreach ($allTables as $table) {
$tableName = $table->TABLE_NAME;
// Remove prefix to get base table name
$baseTableName = str_replace($prefix, '', $tableName);
$tables[] = $baseTableName;
}
return $tables;
} catch (Exception $e) {
Log::error('Error getting mageworx M1 tables: ' . $e->getMessage());
return [];
}
}
/**
* Get all mageworx option templates tables from M2
*/
protected function getMageworxM2Tables()
{
try {
$tables = [];
$database = config("database.connections.{$this->magento2Connection}.database");
$prefix = $this->magento2Prefix;
// Get all tables from information_schema
$allTables = DB::connection($this->magento2Connection)
->select("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?", [
$database,
$prefix . 'mageworx_optiontemplates_%'
]);
foreach ($allTables as $table) {
$tableName = $table->TABLE_NAME;
// Remove prefix to get base table name
$baseTableName = str_replace($prefix, '', $tableName);
$tables[] = $baseTableName;
}
return $tables;
} catch (Exception $e) {
Log::error('Error getting mageworx M2 tables: ' . $e->getMessage());
return [];
}
}
/**
* Get table name mapping from M1 to M2
*/
protected function getMageworxTableMapping()
{
return [
// Singular form mappings
'mageworx_custom_option_template' => 'mageworx_optiontemplates_group',
'mageworx_custom_option_group' => 'mageworx_optiontemplates_group',
'mageworx_custom_option_option' => 'mageworx_optiontemplates_group_option',
'mageworx_custom_option_value' => 'mageworx_optiontemplates_option_type_value',
'mageworx_custom_option_type' => 'mageworx_optiontemplates_option_type',
'mageworx_custom_option_store' => 'mageworx_optiontemplates_store',
// Plural form mappings (actual M1 table names)
// Note: M2 uses "group_option_" prefix structure, not just "option_"
'mageworx_custom_options_group' => 'mageworx_optiontemplates_group',
'mageworx_custom_options_group_store' => 'mageworx_optiontemplates_group_option_store_view', // M2 uses store_view
'mageworx_custom_options_option_default' => null, // May not exist in M2 or merged into another table
'mageworx_custom_options_option_description' => 'mageworx_optiontemplates_group_option_description',
'mageworx_custom_options_option_type_description' => 'mageworx_optiontemplates_group_option_type_description',
'mageworx_custom_options_option_type_image' => 'mageworx_optiontemplates_group_option_type_image',
'mageworx_custom_options_option_type_special_price' => null, // May not exist in M2 or merged into price table
'mageworx_custom_options_option_type_tier_price' => null, // May not exist in M2 or merged into price table
'mageworx_custom_options_option_view_mode' => null, // May not exist in M2
'mageworx_custom_options_relation' => 'mageworx_optiontemplates_relation', // M2 table exists
];
}
/**
* Get M2 table name for M1 table
*/
protected function getM2TableName($m1Table, $tableMapping)
{
// Direct mapping (including null values for tables that don't exist in M2)
if (isset($tableMapping[$m1Table])) {
return $tableMapping[$m1Table];
}
// Try pattern matching for singular form (e.g., mageworx_custom_option_* -> mageworx_optiontemplates_*)
if (strpos($m1Table, 'mageworx_custom_option_') === 0) {
$suffix = str_replace('mageworx_custom_option_', '', $m1Table);
// M2 uses "group_option_" structure for option-related tables
if (strpos($suffix, 'option_') === 0) {
return 'mageworx_optiontemplates_group_' . $suffix;
}
return 'mageworx_optiontemplates_' . $suffix;
}
// Try pattern matching for plural form (e.g., mageworx_custom_options_* -> mageworx_optiontemplates_*)
if (strpos($m1Table, 'mageworx_custom_options_') === 0) {
// Explicitly skip view_mode table
if ($m1Table === 'mageworx_custom_options_option_view_mode') {
return null;
}
// Explicitly skip option_default table
if ($m1Table === 'mageworx_custom_options_option_default') {
return null;
}
$suffix = str_replace('mageworx_custom_options_', '', $m1Table);
// Handle special cases for M2 structure
// M2 uses "group_option_" prefix for option-related tables
if (strpos($suffix, 'option_') === 0) {
return 'mageworx_optiontemplates_group_' . $suffix;
}
// Handle group_store -> group_option_store_view
if ($suffix === 'group_store') {
return 'mageworx_optiontemplates_group_option_store_view';
}
// Default: just replace prefix
return 'mageworx_optiontemplates_' . $suffix;
}
return null;
}
/**
* Check if table exists
*/
protected function tableExists($connection, $tableName)
{
try {
DB::connection($connection)
->table($tableName)
->limit(1)
->first();
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Map column names from M1 to M2 if needed
*/
protected function mapMageworxColumns($m1Table, $m2Table, $rowArray)
{
// Table-specific column mappings
$tableColumnMappings = [
'mageworx_optiontemplates_group_option_type_image' => [
'image_file' => 'image', // M2 uses 'image' instead of 'image_file'
],
];
// Common column mappings
$columnMappings = [
// Generic mappings that might apply
'custom_option_id' => 'option_id',
'custom_option_group_id' => 'group_id',
];
// Get table-specific mappings if they exist
$tableMappings = $tableColumnMappings[$m2Table] ?? [];
$mappedRow = [];
foreach ($rowArray as $key => $value) {
// First check table-specific mapping, then common mapping, then use original key
if (isset($tableMappings[$key])) {
$mappedKey = $tableMappings[$key];
} elseif (isset($columnMappings[$key])) {
$mappedKey = $columnMappings[$key];
} else {
$mappedKey = $key;
}
$mappedRow[$mappedKey] = $value;
}
return $mappedRow;
}
/**
* Check if record exists in M2 table
*/
protected function recordExists($m2Table, $rowArray)
{
try {
$table = $this->magento2Prefix . $m2Table;
// Try common primary key columns in order of preference
$primaryKeys = ['id', 'option_id', 'group_id', 'value_id', 'type_id', 'store_id'];
foreach ($primaryKeys as $key) {
if (isset($rowArray[$key]) && $rowArray[$key] !== null) {
$exists = DB::connection($this->magento2Connection)
->table($table)
->where($key, $rowArray[$key])
->exists();
if ($exists) {
return true;
}
}
}
return false;
} catch (Exception $e) {
Log::warning("Error checking if record exists in {$m2Table}: " . $e->getMessage());
return false;
}
}
/**
* Sort tables by dependency order (parent tables first)
*/
protected function sortTablesByDependency($m1Tables, $tableMapping)
{
// Define table dependency order (parent tables first)
$dependencyOrder = [
'mageworx_optiontemplates_group', // Parent - no dependencies
'mageworx_optiontemplates_group_option', // Depends on group
'mageworx_optiontemplates_group_option_type_value', // Depends on group_option
'mageworx_optiontemplates_group_option_description', // Depends on group_option
'mageworx_optiontemplates_group_option_title', // Depends on group_option
'mageworx_optiontemplates_group_option_price', // Depends on group_option
'mageworx_optiontemplates_group_option_type_description', // Depends on group_option_type_value
'mageworx_optiontemplates_group_option_type_image', // Depends on group_option_type_value
'mageworx_optiontemplates_group_option_store_view', // Depends on group_option
'mageworx_optiontemplates_relation', // Depends on group
];
$sorted = [];
$unsorted = [];
// First, add tables in dependency order
foreach ($dependencyOrder as $m2TableName) {
// Find M1 table that maps to this M2 table
foreach ($m1Tables as $m1Table) {
$m2Table = $this->getM2TableName($m1Table, $tableMapping);
if ($m2Table === $m2TableName) {
$sorted[] = $m1Table;
break;
}
}
}
// Add any remaining tables that weren't in the dependency list
foreach ($m1Tables as $m1Table) {
if (!in_array($m1Table, $sorted)) {
$sorted[] = $m1Table;
}
}
return $sorted;
}
/**
* Validate foreign key constraints before inserting/updating
*/
protected function validateForeignKeyConstraints($m2Table, $mappedRow, $dryRun = false)
{
$prefix = $dryRun ? "Would skip" : "Skipping";
// Validate option_id foreign key for option_description
if ($m2Table === 'mageworx_optiontemplates_group_option_description' && isset($mappedRow['option_id'])) {
$optionExists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'mageworx_optiontemplates_group_option')
->where('option_id', $mappedRow['option_id'])
->exists();
if (!$optionExists) {
$this->migrationLog[] = "WARNING: {$prefix} option_description record - option_id {$mappedRow['option_id']} does not exist in mageworx_optiontemplates_group_option";
return false;
}
}
// Validate option_type_id foreign key for option_type_image
if ($m2Table === 'mageworx_optiontemplates_group_option_type_image' && isset($mappedRow['option_type_id'])) {
$typeExists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'mageworx_optiontemplates_group_option_type_value')
->where('option_type_id', $mappedRow['option_type_id'])
->exists();
if (!$typeExists) {
$this->migrationLog[] = "WARNING: {$prefix} option_type_image record - option_type_id {$mappedRow['option_type_id']} does not exist in mageworx_optiontemplates_group_option_type_value";
return false;
}
}
// Validate group_id foreign key for relation table
if ($m2Table === 'mageworx_optiontemplates_relation' && isset($mappedRow['group_id'])) {
$groupExists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'mageworx_optiontemplates_group')
->where('group_id', $mappedRow['group_id'])
->exists();
if (!$groupExists) {
$this->migrationLog[] = "WARNING: {$prefix} relation record - group_id {$mappedRow['group_id']} does not exist in mageworx_optiontemplates_group";
return false;
}
}
return true;
}
/**
* Get columns that exist in M2 table
*/
protected function getM2TableColumns($m2Table)
{
static $columnCache = [];
if (isset($columnCache[$m2Table])) {
return $columnCache[$m2Table];
}
try {
$database = config("database.connections.{$this->magento2Connection}.database");
$prefix = $this->magento2Prefix;
$columns = DB::connection($this->magento2Connection)
->select("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", [
$database,
$prefix . $m2Table
]);
$columnNames = array_map(function($col) {
return $col->COLUMN_NAME;
}, $columns);
$columnCache[$m2Table] = $columnNames;
return $columnNames;
} catch (Exception $e) {
Log::warning("Error getting columns for {$m2Table}: " . $e->getMessage());
return [];
}
}
/**
* Insert record into M2 table
*/
protected function insertM2Record($m2Table, $rowArray)
{
$table = $this->magento2Prefix . $m2Table;
// Get valid columns for this table
$validColumns = $this->getM2TableColumns($m2Table);
// Filter row to only include columns that exist in M2 table
$filteredRow = [];
foreach ($rowArray as $key => $value) {
if (in_array($key, $validColumns)) {
$filteredRow[$key] = $value;
} else {
$this->migrationLog[] = "INFO: Skipping column '{$key}' - does not exist in M2 table {$m2Table}";
}
}
// Remove null values
$cleanRow = array_filter($filteredRow, function($value) {
return $value !== null;
});
if (empty($cleanRow)) {
$this->migrationLog[] = "WARNING: No valid columns to insert for record in {$m2Table}";
return;
}
DB::connection($this->magento2Connection)
->table($table)
->insert($cleanRow);
}
/**
* Update record in M2 table
*/
protected function updateM2Record($m2Table, $rowArray)
{
$table = $this->magento2Prefix . $m2Table;
// Find primary key
$primaryKeys = ['id', 'option_id', 'group_id', 'value_id', 'type_id', 'store_id'];
$whereClause = [];
foreach ($primaryKeys as $key) {
if (isset($rowArray[$key]) && $rowArray[$key] !== null) {
$whereClause[$key] = $rowArray[$key];
break;
}
}
if (empty($whereClause)) {
throw new Exception("Cannot update record: no primary key found in table {$m2Table}");
}
// Remove primary key from update data
$updateData = $rowArray;
foreach ($primaryKeys as $key) {
unset($updateData[$key]);
}
// Get valid columns for this table
$validColumns = $this->getM2TableColumns($m2Table);
// Filter update data to only include columns that exist in M2 table
$filteredData = [];
foreach ($updateData as $key => $value) {
if (in_array($key, $validColumns)) {
$filteredData[$key] = $value;
}
}
// Remove null values
$cleanData = array_filter($filteredData, function($value) {
return $value !== null;
});
if (!empty($cleanData)) {
DB::connection($this->magento2Connection)
->table($table)
->where($whereClause)
->update($cleanData);
}
}
}