7164 lines
304 KiB
PHP
7164 lines
304 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Exception;
|
|
|
|
class MagentoCategoryMigrationService
|
|
{
|
|
protected $magento1Connection;
|
|
protected $magento2Connection;
|
|
protected $magento1Prefix;
|
|
protected $magento2Prefix;
|
|
protected $storeMapping = [];
|
|
protected $categoryMapping = [];
|
|
protected $migrationLog = [];
|
|
protected $addedCount = 0;
|
|
protected $existingCount = 0;
|
|
protected $columnMapCache = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->magento1Connection = 'magento1';
|
|
$this->magento2Connection = 'magento2';
|
|
$this->magento1Prefix = config('database.connections.magento1.prefix', '');
|
|
$this->magento2Prefix = config('database.connections.magento2.prefix', '');
|
|
}
|
|
|
|
/**
|
|
* Get all stores from Magento 1
|
|
*/
|
|
public 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
|
|
*/
|
|
public 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([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all categories from Magento 1 for a specific store
|
|
*/
|
|
public function getMagento1Categories($storeId = null)
|
|
{
|
|
try {
|
|
// Get entity type ID
|
|
$entityTypeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get attribute IDs
|
|
$nameAttributeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'name')
|
|
->value('attribute_id');
|
|
|
|
$isActiveAttributeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'is_active')
|
|
->value('attribute_id');
|
|
|
|
$urlKeyAttributeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'url_key')
|
|
->value('attribute_id');
|
|
|
|
$targetStoreId = $storeId ?? 0;
|
|
|
|
// Get base category data
|
|
$categories = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'catalog_category_entity')
|
|
->select('entity_id', 'parent_id', 'level', 'path', 'position')
|
|
->orderBy('level')
|
|
->orderBy('position')
|
|
->get();
|
|
|
|
// Get attribute values
|
|
$nameValues = [];
|
|
$isActiveValues = [];
|
|
$urlKeyValues = [];
|
|
|
|
if ($nameAttributeId) {
|
|
$nameValues = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'catalog_category_entity_varchar')
|
|
->where('attribute_id', $nameAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
if ($isActiveAttributeId) {
|
|
$isActiveValues = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'catalog_category_entity_int')
|
|
->where('attribute_id', $isActiveAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
if ($urlKeyAttributeId) {
|
|
$urlKeyValues = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'catalog_category_entity_varchar')
|
|
->where('attribute_id', $urlKeyAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
// Combine data
|
|
return $categories->map(function($category) use ($nameValues, $isActiveValues, $urlKeyValues) {
|
|
$category->name = $nameValues[$category->entity_id] ?? null;
|
|
$category->is_active = $isActiveValues[$category->entity_id] ?? null;
|
|
$category->url_key = $urlKeyValues[$category->entity_id] ?? null;
|
|
return $category;
|
|
});
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 1 categories: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all categories from Magento 2 for a specific store
|
|
*/
|
|
public function getMagento2Categories($storeId = null)
|
|
{
|
|
try {
|
|
// Get entity type ID
|
|
$entityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get attribute IDs
|
|
$nameAttributeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'name')
|
|
->value('attribute_id');
|
|
|
|
$isActiveAttributeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'is_active')
|
|
->value('attribute_id');
|
|
|
|
$urlKeyAttributeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'url_key')
|
|
->value('attribute_id');
|
|
|
|
$targetStoreId = $storeId ?? 0;
|
|
|
|
// Get base category data
|
|
$categories = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->select('entity_id', 'parent_id', 'level', 'path', 'position')
|
|
->orderBy('level')
|
|
->orderBy('position')
|
|
->get();
|
|
|
|
// Get attribute values
|
|
$nameValues = [];
|
|
$isActiveValues = [];
|
|
$urlKeyValues = [];
|
|
|
|
if ($nameAttributeId) {
|
|
$nameValues = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity_varchar')
|
|
->where('attribute_id', $nameAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
if ($isActiveAttributeId) {
|
|
$isActiveValues = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity_int')
|
|
->where('attribute_id', $isActiveAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
if ($urlKeyAttributeId) {
|
|
$urlKeyValues = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity_varchar')
|
|
->where('attribute_id', $urlKeyAttributeId)
|
|
->where('store_id', $targetStoreId)
|
|
->pluck('value', 'entity_id')
|
|
->toArray();
|
|
}
|
|
|
|
// Combine data
|
|
return $categories->map(function($category) use ($nameValues, $isActiveValues, $urlKeyValues) {
|
|
$category->name = $nameValues[$category->entity_id] ?? null;
|
|
$category->is_active = $isActiveValues[$category->entity_id] ?? null;
|
|
$category->url_key = $urlKeyValues[$category->entity_id] ?? null;
|
|
return $category;
|
|
});
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 2 categories: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find categories in Magento 2 that don't exist in Magento 1
|
|
* Compares by category name (case-insensitive)
|
|
*/
|
|
public function getM2CategoriesNotInM1()
|
|
{
|
|
try {
|
|
$m1Categories = $this->getMagento1Categories();
|
|
$m2Categories = $this->getMagento2Categories();
|
|
|
|
// Create a set of M1 category names (case-insensitive, normalized)
|
|
$m1CategoryNames = $m1Categories->map(function($cat) {
|
|
return strtolower(trim($cat->name ?? ''));
|
|
})->filter(function($name) {
|
|
return !empty($name);
|
|
})->unique()->toArray();
|
|
|
|
// Get all M2 categories to build a map for root category lookup
|
|
$m2CategoryMap = [];
|
|
foreach ($m2Categories as $cat) {
|
|
$m2CategoryMap[$cat->entity_id] = $cat;
|
|
}
|
|
|
|
// Find M2 categories that don't have a matching name in M1
|
|
$missingCategories = $m2Categories->filter(function($m2Cat) use ($m1CategoryNames) {
|
|
$m2Name = strtolower(trim($m2Cat->name ?? ''));
|
|
return !empty($m2Name) && !in_array($m2Name, $m1CategoryNames);
|
|
})->map(function($m2Cat) use ($m2CategoryMap) {
|
|
// Extract root category ID from path (typically second element: 1/2/3/4 -> 2)
|
|
$rootCategoryId = null;
|
|
$rootCategoryName = 'N/A';
|
|
|
|
if (!empty($m2Cat->path)) {
|
|
$pathParts = explode('/', $m2Cat->path);
|
|
// Root category is typically at index 1 (second element)
|
|
if (count($pathParts) > 1 && isset($pathParts[1])) {
|
|
$rootCategoryId = (int)$pathParts[1];
|
|
|
|
// Get root category name if it exists in our map
|
|
if (isset($m2CategoryMap[$rootCategoryId])) {
|
|
$rootCategoryName = $m2CategoryMap[$rootCategoryId]->name ?? "ID: {$rootCategoryId}";
|
|
} else {
|
|
$rootCategoryName = "ID: {$rootCategoryId}";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add root category info to the category object
|
|
$m2Cat->root_category_id = $rootCategoryId;
|
|
$m2Cat->root_category_name = $rootCategoryName;
|
|
|
|
return $m2Cat;
|
|
})->values();
|
|
|
|
return $missingCategories;
|
|
} catch (Exception $e) {
|
|
Log::error('Error finding M2 categories not in M1: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find categories in Magento 1 that don't exist in Magento 2
|
|
* Compares by category name (case-insensitive)
|
|
*/
|
|
public function getM1CategoriesNotInM2()
|
|
{
|
|
try {
|
|
$m1Categories = $this->getMagento1Categories();
|
|
$m2Categories = $this->getMagento2Categories();
|
|
|
|
// Create a set of M2 category names (case-insensitive, normalized)
|
|
$m2CategoryNames = $m2Categories->map(function($cat) {
|
|
return strtolower(trim($cat->name ?? ''));
|
|
})->filter(function($name) {
|
|
return !empty($name);
|
|
})->unique()->toArray();
|
|
|
|
// Get all M1 categories to build a map for root category lookup
|
|
$m1CategoryMap = [];
|
|
foreach ($m1Categories as $cat) {
|
|
$m1CategoryMap[$cat->entity_id] = $cat;
|
|
}
|
|
|
|
// Find M1 categories that don't have a matching name in M2
|
|
$missingCategories = $m1Categories->filter(function($m1Cat) use ($m2CategoryNames) {
|
|
$m1Name = strtolower(trim($m1Cat->name ?? ''));
|
|
return !empty($m1Name) && !in_array($m1Name, $m2CategoryNames);
|
|
})->map(function($m1Cat) use ($m1CategoryMap) {
|
|
// Extract root category ID from path (typically second element: 1/2/3/4 -> 2)
|
|
$rootCategoryId = null;
|
|
$rootCategoryName = 'N/A';
|
|
|
|
if (!empty($m1Cat->path)) {
|
|
$pathParts = explode('/', $m1Cat->path);
|
|
// Root category is typically at index 1 (second element)
|
|
if (count($pathParts) > 1 && isset($pathParts[1])) {
|
|
$rootCategoryId = (int)$pathParts[1];
|
|
|
|
// Get root category name if it exists in our map
|
|
if (isset($m1CategoryMap[$rootCategoryId])) {
|
|
$rootCategoryName = $m1CategoryMap[$rootCategoryId]->name ?? "ID: {$rootCategoryId}";
|
|
} else {
|
|
$rootCategoryName = "ID: {$rootCategoryId}";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add root category info to the category object
|
|
$m1Cat->root_category_id = $rootCategoryId;
|
|
$m1Cat->root_category_name = $rootCategoryName;
|
|
|
|
return $m1Cat;
|
|
})->values();
|
|
|
|
return $missingCategories;
|
|
} catch (Exception $e) {
|
|
Log::error('Error finding M1 categories not in M2: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
protected function getCategoryProductCounts($categoryIds, $source = 'm2')
|
|
{
|
|
if (empty($categoryIds)) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection;
|
|
$prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix;
|
|
$tableName = $prefix . 'catalog_category_product';
|
|
|
|
// Check if table exists
|
|
try {
|
|
$productCounts = DB::connection($connection)
|
|
->table($tableName)
|
|
->whereIn('category_id', $categoryIds)
|
|
->select('category_id', DB::raw('COUNT(product_id) as product_count'))
|
|
->groupBy('category_id')
|
|
->pluck('product_count', 'category_id')
|
|
->toArray();
|
|
} catch (Exception $e) {
|
|
// Table might not exist, return empty array
|
|
Log::warning("Table {$tableName} might not exist: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
|
|
return $productCounts;
|
|
} catch (Exception $e) {
|
|
Log::error("Error getting product counts: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build hierarchical category tree structure
|
|
*/
|
|
public function buildCategoryTreeHierarchy($categories, $source = 'm2')
|
|
{
|
|
$categoryMap = [];
|
|
$rootCategories = [];
|
|
|
|
// Get all category IDs
|
|
$categoryIds = $categories->pluck('entity_id')->toArray();
|
|
|
|
// Get product counts for all categories
|
|
$productCounts = $this->getCategoryProductCounts($categoryIds, $source);
|
|
|
|
// First pass: create a map of all categories with children array
|
|
foreach ($categories as $category) {
|
|
$categoryId = $category->entity_id;
|
|
$categoryMap[$categoryId] = [
|
|
'id' => $categoryId,
|
|
'name' => $category->name ?? 'Unnamed Category',
|
|
'parent_id' => $category->parent_id,
|
|
'level' => $category->level ?? 0,
|
|
'position' => $category->position ?? 0,
|
|
'is_active' => $category->is_active ?? 0,
|
|
'path' => $category->path ?? '',
|
|
'product_count' => $productCounts[$categoryId] ?? 0,
|
|
'children' => []
|
|
];
|
|
}
|
|
|
|
// Second pass: build the tree structure by assigning children to parents
|
|
foreach ($categoryMap as $id => $category) {
|
|
$parentId = $category['parent_id'];
|
|
|
|
// Skip if parent is self (prevent circular references)
|
|
if ($parentId == $id) {
|
|
$rootCategories[] = &$categoryMap[$id];
|
|
continue;
|
|
}
|
|
|
|
// Check if this is a root category (parent is 0, 1, or doesn't exist in map)
|
|
if ($parentId == 0 || $parentId == 1 || !isset($categoryMap[$parentId])) {
|
|
$rootCategories[] = &$categoryMap[$id];
|
|
} else {
|
|
// Add as child of parent
|
|
$categoryMap[$parentId]['children'][] = &$categoryMap[$id];
|
|
}
|
|
}
|
|
|
|
// Sort function for categories
|
|
$sortCategories = function(&$categories) use (&$sortCategories) {
|
|
usort($categories, function($a, $b) {
|
|
return $a['position'] <=> $b['position'];
|
|
});
|
|
// Recursively sort children
|
|
foreach ($categories as &$category) {
|
|
if (!empty($category['children'])) {
|
|
$sortCategories($category['children']);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Sort root categories and all children recursively
|
|
$sortCategories($rootCategories);
|
|
|
|
return $rootCategories;
|
|
}
|
|
|
|
/**
|
|
* Migrate categories from Magento 1 to Magento 2
|
|
*/
|
|
public function migrateCategories($storeMapping = [])
|
|
{
|
|
$this->storeMapping = $storeMapping;
|
|
$this->migrationLog = [];
|
|
$this->categoryMapping = [];
|
|
$this->addedCount = 0;
|
|
$this->existingCount = 0;
|
|
|
|
try {
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
// Get root category ID for Magento 2 (usually 2)
|
|
$rootCategoryId = $this->getMagento2RootCategoryId();
|
|
|
|
// Get all categories from Magento 1
|
|
$m1Categories = $this->getMagento1Categories();
|
|
|
|
// Build category tree
|
|
$categoryTree = $this->buildCategoryTree($m1Categories);
|
|
|
|
// Migrate categories level by level
|
|
foreach ($categoryTree as $level => $categories) {
|
|
foreach ($categories as $m1Category) {
|
|
$this->migrateCategory($m1Category, $rootCategoryId);
|
|
}
|
|
}
|
|
|
|
// Update children_count for all affected parent categories
|
|
$this->updateChildrenCounts();
|
|
|
|
// Migrate category attributes for each store
|
|
foreach ($this->storeMapping as $m1StoreId => $m2StoreId) {
|
|
$this->migrateCategoryAttributes($m1StoreId, $m2StoreId);
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Categories migrated successfully',
|
|
'migrated_count' => count($this->categoryMapping),
|
|
'added_count' => $this->addedCount,
|
|
'existing_count' => $this->existingCount,
|
|
'log' => $this->migrationLog
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
Log::error('Category migration error: ' . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Migration failed: ' . $e->getMessage(),
|
|
'log' => $this->migrationLog
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build category tree organized by level
|
|
*/
|
|
protected function buildCategoryTree($categories)
|
|
{
|
|
$tree = [];
|
|
foreach ($categories as $category) {
|
|
$level = $category->level ?? 1;
|
|
if (!isset($tree[$level])) {
|
|
$tree[$level] = [];
|
|
}
|
|
$tree[$level][] = $category;
|
|
}
|
|
ksort($tree);
|
|
return $tree;
|
|
}
|
|
|
|
/**
|
|
* Find existing category in Magento 2 by name and parent
|
|
*/
|
|
protected function findExistingCategoryInM2($categoryName, $parentId)
|
|
{
|
|
if (!$categoryName) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Get name attribute ID for M2
|
|
$entityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return null;
|
|
}
|
|
|
|
$nameAttributeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->where('attribute_code', 'name')
|
|
->value('attribute_id');
|
|
|
|
if (!$nameAttributeId) {
|
|
return null;
|
|
}
|
|
|
|
// Find categories with the same name and parent (checking default store view)
|
|
$existingCategories = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity', 'e')
|
|
->join($this->magento2Prefix . 'catalog_category_entity_varchar as v', function($join) use ($nameAttributeId) {
|
|
$join->on('e.entity_id', '=', 'v.entity_id')
|
|
->where('v.attribute_id', '=', $nameAttributeId)
|
|
->where('v.store_id', '=', 0);
|
|
})
|
|
->where('v.value', '=', $categoryName)
|
|
->where('e.parent_id', '=', $parentId)
|
|
->select('e.entity_id')
|
|
->first();
|
|
|
|
return $existingCategories ? $existingCategories->entity_id : null;
|
|
|
|
} catch (Exception $e) {
|
|
Log::error("Error finding existing category in M2: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate a single category
|
|
*/
|
|
protected function migrateCategory($m1Category, $parentId = null)
|
|
{
|
|
try {
|
|
// Check if category already exists in mapping (already processed in this migration)
|
|
if (isset($this->categoryMapping[$m1Category->entity_id])) {
|
|
return $this->categoryMapping[$m1Category->entity_id];
|
|
}
|
|
|
|
// Determine parent ID
|
|
if ($m1Category->parent_id == 1 || $m1Category->parent_id == 0) {
|
|
$m2ParentId = $parentId ?? $this->getMagento2RootCategoryId();
|
|
} else {
|
|
$m2ParentId = $this->categoryMapping[$m1Category->parent_id] ?? $parentId;
|
|
}
|
|
|
|
// Check if category already exists in M2 database
|
|
$existingM2CategoryId = null;
|
|
if ($m1Category->name) {
|
|
$existingM2CategoryId = $this->findExistingCategoryInM2($m1Category->name, $m2ParentId);
|
|
}
|
|
|
|
if ($existingM2CategoryId) {
|
|
// Category already exists in M2, use existing ID
|
|
$m2EntityId = $existingM2CategoryId;
|
|
$this->existingCount++;
|
|
$this->migrationLog[] = "Found existing category: ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})";
|
|
} else {
|
|
// Category doesn't exist, create new one
|
|
$m2EntityId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->insertGetId([
|
|
'attribute_set_id' => 3, // Default category attribute set
|
|
'parent_id' => $m2ParentId,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'path' => '',
|
|
'position' => $m1Category->position ?? 0,
|
|
'level' => $m1Category->level ?? 1,
|
|
'children_count' => 0,
|
|
]);
|
|
|
|
// Update path
|
|
$path = $this->buildCategoryPath($m2EntityId, $m2ParentId);
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $m2EntityId)
|
|
->update(['path' => $path]);
|
|
|
|
$this->addedCount++;
|
|
$this->migrationLog[] = "Added new category: ID {$m1Category->entity_id} -> {$m2EntityId} ({$m1Category->name})";
|
|
}
|
|
|
|
// Store mapping
|
|
$this->categoryMapping[$m1Category->entity_id] = $m2EntityId;
|
|
|
|
return $m2EntityId;
|
|
|
|
} catch (Exception $e) {
|
|
Log::error("Error migrating category {$m1Category->entity_id}: " . $e->getMessage());
|
|
$this->migrationLog[] = "ERROR: Failed to migrate category ID {$m1Category->entity_id}: " . $e->getMessage();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update children_count for all parent categories
|
|
*/
|
|
protected function updateChildrenCounts()
|
|
{
|
|
try {
|
|
// Get all unique parent IDs from migrated categories
|
|
$parentIds = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->whereIn('entity_id', array_values($this->categoryMapping))
|
|
->distinct()
|
|
->pluck('parent_id')
|
|
->toArray();
|
|
|
|
// Update children_count for each parent
|
|
foreach ($parentIds as $parentId) {
|
|
if ($parentId > 0) {
|
|
$childrenCount = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('parent_id', $parentId)
|
|
->count();
|
|
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $parentId)
|
|
->update(['children_count' => $childrenCount]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::error("Error updating children counts: " . $e->getMessage());
|
|
// Don't throw, just log the error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build category path
|
|
*/
|
|
protected function buildCategoryPath($entityId, $parentId)
|
|
{
|
|
if ($parentId == $this->getMagento2RootCategoryId()) {
|
|
return "1/{$parentId}/{$entityId}";
|
|
}
|
|
|
|
$parent = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $parentId)
|
|
->first();
|
|
|
|
if ($parent && $parent->path) {
|
|
return $parent->path . '/' . $entityId;
|
|
}
|
|
|
|
return "1/{$parentId}/{$entityId}";
|
|
}
|
|
|
|
/**
|
|
* Migrate category attributes for a specific store
|
|
*/
|
|
protected function migrateCategoryAttributes($m1StoreId, $m2StoreId)
|
|
{
|
|
try {
|
|
// Get attribute IDs for Magento 2
|
|
$attributeIds = $this->getMagento2AttributeIds();
|
|
|
|
// Get categories with their attributes from Magento 1
|
|
$m1Categories = $this->getMagento1Categories($m1StoreId);
|
|
|
|
foreach ($m1Categories as $m1Category) {
|
|
if (!isset($this->categoryMapping[$m1Category->entity_id])) {
|
|
continue;
|
|
}
|
|
|
|
$m2EntityId = $this->categoryMapping[$m1Category->entity_id];
|
|
|
|
// Migrate name
|
|
if ($m1Category->name) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['name'],
|
|
$m2StoreId,
|
|
$m1Category->name
|
|
);
|
|
}
|
|
|
|
// Migrate is_active
|
|
if ($m1Category->is_active !== null) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['is_active'],
|
|
$m2StoreId,
|
|
$m1Category->is_active
|
|
);
|
|
}
|
|
|
|
// Migrate url_key
|
|
if ($m1Category->url_key) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['url_key'],
|
|
$m2StoreId,
|
|
$m1Category->url_key
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->migrationLog[] = "Migrated attributes for store mapping: M1 Store {$m1StoreId} -> M2 Store {$m2StoreId}";
|
|
|
|
} catch (Exception $e) {
|
|
Log::error("Error migrating category attributes: " . $e->getMessage());
|
|
$this->migrationLog[] = "ERROR: Failed to migrate attributes for store {$m1StoreId}: " . $e->getMessage();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert category attribute value
|
|
*/
|
|
protected function insertCategoryAttribute($entityId, $attributeId, $storeId, $value)
|
|
{
|
|
// Determine which table to use based on attribute type
|
|
$attribute = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('attribute_id', $attributeId)
|
|
->first();
|
|
|
|
if (!$attribute) {
|
|
return;
|
|
}
|
|
|
|
$table = $this->magento2Prefix . 'catalog_category_entity_' . $attribute->backend_type;
|
|
|
|
// Check if value already exists
|
|
$exists = DB::connection($this->magento2Connection)
|
|
->table($table)
|
|
->where('entity_id', $entityId)
|
|
->where('attribute_id', $attributeId)
|
|
->where('store_id', $storeId)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
DB::connection($this->magento2Connection)
|
|
->table($table)
|
|
->where('entity_id', $entityId)
|
|
->where('attribute_id', $attributeId)
|
|
->where('store_id', $storeId)
|
|
->update(['value' => $value]);
|
|
} else {
|
|
DB::connection($this->magento2Connection)
|
|
->table($table)
|
|
->insert([
|
|
'attribute_id' => $attributeId,
|
|
'store_id' => $storeId,
|
|
'entity_id' => $entityId,
|
|
'value' => $value,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Magento 2 attribute IDs
|
|
*/
|
|
protected function getMagento2AttributeIds()
|
|
{
|
|
$entityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
$attributes = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->whereIn('attribute_code', ['name', 'is_active', 'url_key'])
|
|
->pluck('attribute_id', 'attribute_code')
|
|
->toArray();
|
|
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Migrate a single category from Magento 1 to Magento 2
|
|
*/
|
|
public function migrateSingleCategory($m1CategoryId, $storeId = 0)
|
|
{
|
|
try {
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
// Get the M1 category
|
|
$m1Categories = $this->getMagento1Categories($storeId);
|
|
$m1Category = $m1Categories->firstWhere('entity_id', $m1CategoryId);
|
|
|
|
if (!$m1Category) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Category not found in Magento 1',
|
|
];
|
|
}
|
|
|
|
// Determine parent ID in M2
|
|
$m2ParentId = $this->getMagento2RootCategoryId();
|
|
|
|
// If category has a parent in M1, try to find it in M2
|
|
if ($m1Category->parent_id && $m1Category->parent_id != 0 && $m1Category->parent_id != 1) {
|
|
// Check if parent was already migrated (check by name)
|
|
$m1ParentCategories = $this->getMagento1Categories($storeId);
|
|
$m1Parent = $m1ParentCategories->firstWhere('entity_id', $m1Category->parent_id);
|
|
|
|
if ($m1Parent && $m1Parent->name) {
|
|
$m2ParentId = $this->findExistingCategoryInM2($m1Parent->name, $this->getMagento2RootCategoryId());
|
|
if (!$m2ParentId) {
|
|
// Parent not found, use root
|
|
$m2ParentId = $this->getMagento2RootCategoryId();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if category already exists in M2
|
|
$existingM2CategoryId = null;
|
|
if ($m1Category->name) {
|
|
$existingM2CategoryId = $this->findExistingCategoryInM2($m1Category->name, $m2ParentId);
|
|
}
|
|
|
|
if ($existingM2CategoryId) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Category already exists in Magento 2 (ID: ' . $existingM2CategoryId . ')',
|
|
'm2_category_id' => $existingM2CategoryId,
|
|
];
|
|
}
|
|
|
|
// Create new category entity
|
|
$m2EntityId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->insertGetId([
|
|
'attribute_set_id' => 3, // Default category attribute set
|
|
'parent_id' => $m2ParentId,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'path' => '',
|
|
'position' => $m1Category->position ?? 0,
|
|
'level' => $m1Category->level ?? 1,
|
|
'children_count' => 0,
|
|
]);
|
|
|
|
// Update path
|
|
$path = $this->buildCategoryPath($m2EntityId, $m2ParentId);
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $m2EntityId)
|
|
->update(['path' => $path]);
|
|
|
|
// Migrate attributes for default store (0)
|
|
$attributeIds = $this->getMagento2AttributeIds();
|
|
|
|
// Migrate name
|
|
if ($m1Category->name) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['name'],
|
|
$storeId,
|
|
$m1Category->name
|
|
);
|
|
}
|
|
|
|
// Migrate is_active
|
|
if ($m1Category->is_active !== null) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['is_active'],
|
|
$storeId,
|
|
$m1Category->is_active
|
|
);
|
|
}
|
|
|
|
// Migrate url_key
|
|
if ($m1Category->url_key) {
|
|
$this->insertCategoryAttribute(
|
|
$m2EntityId,
|
|
$attributeIds['url_key'],
|
|
$storeId,
|
|
$m1Category->url_key
|
|
);
|
|
}
|
|
|
|
// Update children_count for parent
|
|
if ($m2ParentId > 0) {
|
|
$childrenCount = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('parent_id', $m2ParentId)
|
|
->count();
|
|
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $m2ParentId)
|
|
->update(['children_count' => $childrenCount]);
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Category migrated successfully',
|
|
'm1_category_id' => $m1CategoryId,
|
|
'm2_category_id' => $m2EntityId,
|
|
'category_name' => $m1Category->name ?? 'Unnamed Category',
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
Log::error("Error migrating single category {$m1CategoryId}: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Migration failed: ' . $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rename a Magento 2 category
|
|
*/
|
|
public function renameCategory($categoryId, $newName, $storeId = 0)
|
|
{
|
|
try {
|
|
// Validate category exists
|
|
$category = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('entity_id', $categoryId)
|
|
->first();
|
|
|
|
if (!$category) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Category not found',
|
|
];
|
|
}
|
|
|
|
// Prevent renaming system root
|
|
if ($categoryId == 0 || $categoryId == 1) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Cannot rename system root category',
|
|
];
|
|
}
|
|
|
|
// Validate name
|
|
$newName = trim($newName);
|
|
if (empty($newName)) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Category name cannot be empty',
|
|
];
|
|
}
|
|
|
|
// Get name attribute ID
|
|
$attributeIds = $this->getMagento2AttributeIds();
|
|
if (!isset($attributeIds['name'])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Name attribute not found',
|
|
];
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
// Update name attribute for the specified store (default store = 0)
|
|
$this->insertCategoryAttribute(
|
|
$categoryId,
|
|
$attributeIds['name'],
|
|
$storeId,
|
|
$newName
|
|
);
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Category renamed successfully',
|
|
'new_name' => $newName,
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
if (isset($this->magento2Connection)) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
}
|
|
Log::error("Error renaming category {$categoryId}: " . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to rename category: ' . $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Magento 2 root category ID
|
|
*/
|
|
protected function getMagento2RootCategoryId()
|
|
{
|
|
// Root category is usually ID 2 in Magento 2
|
|
$rootId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'catalog_category_entity')
|
|
->where('level', 0)
|
|
->where('parent_id', 0)
|
|
->value('entity_id');
|
|
|
|
return $rootId ?: 2;
|
|
}
|
|
|
|
/**
|
|
* Delete a category and all its children recursively
|
|
*/
|
|
public function deleteCategory($categoryId, $source = 'm2')
|
|
{
|
|
try {
|
|
$connection = $source === 'm1' ? $this->magento1Connection : $this->magento2Connection;
|
|
$prefix = $source === 'm1' ? $this->magento1Prefix : $this->magento2Prefix;
|
|
|
|
// Get category information
|
|
$category = DB::connection($connection)
|
|
->table($prefix . 'catalog_category_entity')
|
|
->where('entity_id', $categoryId)
|
|
->first();
|
|
|
|
if (!$category) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Category not found',
|
|
];
|
|
}
|
|
|
|
// Prevent deletion of system root (ID 0 or 1)
|
|
if ($categoryId == 0 || $categoryId == 1) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Cannot delete system root category',
|
|
];
|
|
}
|
|
|
|
DB::connection($connection)->beginTransaction();
|
|
|
|
// Recursively delete all children first
|
|
$this->deleteCategoryChildren($categoryId, $connection, $prefix);
|
|
|
|
// Now delete the category itself
|
|
$this->deleteCategoryData($categoryId, $connection, $prefix);
|
|
|
|
// Update parent's children_count if parent exists
|
|
if ($category->parent_id > 0 && $category->parent_id != 1) {
|
|
$parentChildrenCount = DB::connection($connection)
|
|
->table($prefix . 'catalog_category_entity')
|
|
->where('parent_id', $category->parent_id)
|
|
->count();
|
|
|
|
DB::connection($connection)
|
|
->table($prefix . 'catalog_category_entity')
|
|
->where('entity_id', $category->parent_id)
|
|
->update(['children_count' => $parentChildrenCount]);
|
|
}
|
|
|
|
DB::connection($connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Category and all subcategories deleted successfully',
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
if (isset($connection)) {
|
|
DB::connection($connection)->rollBack();
|
|
}
|
|
Log::error("Error deleting category {$categoryId}: " . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to delete category: ' . $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test database connections
|
|
*/
|
|
public function testConnections()
|
|
{
|
|
$results = [
|
|
'magento1' => false,
|
|
'magento2' => false,
|
|
];
|
|
|
|
try {
|
|
DB::connection($this->magento1Connection)->select('SELECT 1');
|
|
$results['magento1'] = true;
|
|
} catch (Exception $e) {
|
|
$results['magento1_error'] = $e->getMessage();
|
|
}
|
|
|
|
try {
|
|
DB::connection($this->magento2Connection)->select('SELECT 1');
|
|
$results['magento2'] = true;
|
|
} catch (Exception $e) {
|
|
$results['magento2_error'] = $e->getMessage();
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Get all category attributes from Magento 1
|
|
*/
|
|
public function getMagento1Attributes()
|
|
{
|
|
try {
|
|
// Get entity type ID for catalog_category
|
|
$entityTypeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get all attributes for catalog_category
|
|
$attributes = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label', 'is_required', 'is_user_defined', 'default_value')
|
|
->orderBy('attribute_code')
|
|
->get();
|
|
|
|
return $attributes;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 1 attributes: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all category attributes from Magento 2
|
|
*/
|
|
public function getMagento2Attributes()
|
|
{
|
|
try {
|
|
// Get entity type ID for catalog_category
|
|
$entityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get all attributes for catalog_category
|
|
$attributes = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label', 'is_required', 'is_user_defined', 'default_value')
|
|
->orderBy('attribute_code')
|
|
->get();
|
|
|
|
return $attributes;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 2 attributes: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get attributes that exist in Magento 1 but are missing in Magento 2
|
|
*/
|
|
public function getM1AttributesMissingInM2()
|
|
{
|
|
try {
|
|
$m1Attributes = $this->getMagento1Attributes();
|
|
$m2Attributes = $this->getMagento2Attributes();
|
|
|
|
// Get all M2 attribute codes
|
|
$m2AttributeCodes = $m2Attributes->pluck('attribute_code')->toArray();
|
|
|
|
// Filter M1 attributes that don't exist in M2
|
|
$missingAttributes = $m1Attributes->filter(function ($attr) use ($m2AttributeCodes) {
|
|
return !in_array($attr->attribute_code, $m2AttributeCodes);
|
|
});
|
|
|
|
return $missingAttributes->values();
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching missing attributes: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate an attribute from Magento 1 to Magento 2
|
|
*/
|
|
public function migrateAttribute($m1AttributeId)
|
|
{
|
|
try {
|
|
// Get M1 entity type ID
|
|
$m1EntityTypeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$m1EntityTypeId) {
|
|
return ['success' => false, 'message' => 'Magento 1 entity type not found'];
|
|
}
|
|
|
|
// Get full attribute data from M1
|
|
$m1Attribute = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('attribute_id', $m1AttributeId)
|
|
->where('entity_type_id', $m1EntityTypeId)
|
|
->first();
|
|
|
|
if (!$m1Attribute) {
|
|
return ['success' => false, 'message' => 'Attribute not found in Magento 1'];
|
|
}
|
|
|
|
// Check if attribute already exists in M2
|
|
$m2EntityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$m2EntityTypeId) {
|
|
return ['success' => false, 'message' => 'Magento 2 entity type not found'];
|
|
}
|
|
|
|
$existingAttribute = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $m2EntityTypeId)
|
|
->where('attribute_code', $m1Attribute->attribute_code)
|
|
->first();
|
|
|
|
if ($existingAttribute) {
|
|
return ['success' => false, 'message' => 'Attribute already exists in Magento 2'];
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
// Insert attribute into M2
|
|
$m2AttributeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->insertGetId([
|
|
'entity_type_id' => $m2EntityTypeId,
|
|
'attribute_code' => $m1Attribute->attribute_code,
|
|
'attribute_model' => $m1Attribute->attribute_model ?? null,
|
|
'backend_model' => $m1Attribute->backend_model ?? null,
|
|
'backend_type' => $m1Attribute->backend_type,
|
|
'backend_table' => $m1Attribute->backend_table ?? null,
|
|
'frontend_model' => $m1Attribute->frontend_model ?? null,
|
|
'frontend_input' => $m1Attribute->frontend_input ?? null,
|
|
'frontend_label' => $m1Attribute->frontend_label ?? null,
|
|
'frontend_class' => $m1Attribute->frontend_class ?? null,
|
|
'source_model' => $m1Attribute->source_model ?? null,
|
|
'is_required' => $m1Attribute->is_required ?? 0,
|
|
'is_user_defined' => $m1Attribute->is_user_defined ?? 1,
|
|
'default_value' => $m1Attribute->default_value ?? null,
|
|
'is_unique' => $m1Attribute->is_unique ?? 0,
|
|
'note' => $m1Attribute->note ?? null,
|
|
]);
|
|
|
|
// Add attribute to default attribute set if it exists
|
|
$defaultAttributeSet = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_set')
|
|
->where('entity_type_id', $m2EntityTypeId)
|
|
->where('attribute_set_name', 'Default')
|
|
->first();
|
|
|
|
if ($defaultAttributeSet) {
|
|
$defaultGroup = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_group')
|
|
->where('attribute_set_id', $defaultAttributeSet->attribute_set_id)
|
|
->orderBy('sort_order')
|
|
->first();
|
|
|
|
if ($defaultGroup) {
|
|
// Get max sort order for this group
|
|
$maxSortOrder = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_attribute')
|
|
->where('attribute_set_id', $defaultAttributeSet->attribute_set_id)
|
|
->where('attribute_group_id', $defaultGroup->attribute_group_id)
|
|
->max('sort_order') ?? 0;
|
|
|
|
// Add attribute to entity_attribute table
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_attribute')
|
|
->insert([
|
|
'entity_type_id' => $m2EntityTypeId,
|
|
'attribute_set_id' => $defaultAttributeSet->attribute_set_id,
|
|
'attribute_group_id' => $defaultGroup->attribute_group_id,
|
|
'attribute_id' => $m2AttributeId,
|
|
'sort_order' => $maxSortOrder + 10,
|
|
]);
|
|
}
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Attribute migrated successfully',
|
|
'attribute_code' => $m1Attribute->attribute_code,
|
|
'm2_attribute_id' => $m2AttributeId,
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
if (isset($this->magento2Connection)) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
}
|
|
Log::error("Error migrating attribute {$m1AttributeId}: " . $e->getMessage());
|
|
return ['success' => false, 'message' => 'Failed to migrate attribute: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get attribute groups from Magento 1
|
|
*/
|
|
public function getMagento1AttributeGroups()
|
|
{
|
|
try {
|
|
// Get entity type ID for catalog_category
|
|
$entityTypeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get attribute sets for catalog_category
|
|
$attributeSets = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute_set')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->get();
|
|
|
|
$groups = collect([]);
|
|
|
|
foreach ($attributeSets as $set) {
|
|
// Get attribute groups for this set
|
|
$setGroups = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute_group')
|
|
->where('attribute_set_id', $set->attribute_set_id)
|
|
->select('attribute_group_id', 'attribute_set_id', 'attribute_group_name', 'sort_order')
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
foreach ($setGroups as $group) {
|
|
// Get attributes in this group
|
|
$attributeIds = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_attribute')
|
|
->where('attribute_set_id', $set->attribute_set_id)
|
|
->where('attribute_group_id', $group->attribute_group_id)
|
|
->pluck('attribute_id')
|
|
->toArray();
|
|
|
|
$group->attribute_set_name = $set->attribute_set_name ?? 'Default';
|
|
$group->attribute_count = count($attributeIds);
|
|
$group->attribute_ids = $attributeIds;
|
|
$groups->push($group);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 1 attribute groups: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get attribute groups from Magento 2
|
|
*/
|
|
public function getMagento2AttributeGroups()
|
|
{
|
|
try {
|
|
// Get entity type ID for catalog_category
|
|
$entityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$entityTypeId) {
|
|
return collect([]);
|
|
}
|
|
|
|
// Get attribute sets for catalog_category
|
|
$attributeSets = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_set')
|
|
->where('entity_type_id', $entityTypeId)
|
|
->get();
|
|
|
|
$groups = collect([]);
|
|
|
|
foreach ($attributeSets as $set) {
|
|
// Get attribute groups for this set
|
|
$setGroups = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_group')
|
|
->where('attribute_set_id', $set->attribute_set_id)
|
|
->select('attribute_group_id', 'attribute_set_id', 'attribute_group_name', 'sort_order')
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
foreach ($setGroups as $group) {
|
|
// Get attributes in this group
|
|
$attributeIds = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_attribute')
|
|
->where('attribute_set_id', $set->attribute_set_id)
|
|
->where('attribute_group_id', $group->attribute_group_id)
|
|
->pluck('attribute_id')
|
|
->toArray();
|
|
|
|
$group->attribute_set_name = $set->attribute_set_name ?? 'Default';
|
|
$group->attribute_count = count($attributeIds);
|
|
$group->attribute_ids = $attributeIds;
|
|
$groups->push($group);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 2 attribute groups: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get attribute groups that exist in Magento 1 but are missing in Magento 2
|
|
*/
|
|
public function getM1AttributeGroupsMissingInM2()
|
|
{
|
|
try {
|
|
$m1Groups = $this->getMagento1AttributeGroups();
|
|
$m2Groups = $this->getMagento2AttributeGroups();
|
|
|
|
// Create a map of M2 groups by set name and group name
|
|
$m2GroupMap = [];
|
|
foreach ($m2Groups as $m2Group) {
|
|
$key = ($m2Group->attribute_set_name ?? 'Default') . '|' . ($m2Group->attribute_group_name ?? '');
|
|
$m2GroupMap[$key] = true;
|
|
}
|
|
|
|
// Filter M1 groups that don't exist in M2
|
|
$missingGroups = $m1Groups->filter(function ($group) use ($m2GroupMap) {
|
|
$key = ($group->attribute_set_name ?? 'Default') . '|' . ($group->attribute_group_name ?? '');
|
|
return !isset($m2GroupMap[$key]);
|
|
});
|
|
|
|
return $missingGroups->values();
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching missing attribute groups: ' . $e->getMessage());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate an attribute group from Magento 1 to Magento 2
|
|
*/
|
|
public function migrateAttributeGroup($m1GroupId, $m1SetId)
|
|
{
|
|
try {
|
|
// Get M1 entity type ID
|
|
$m1EntityTypeId = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$m1EntityTypeId) {
|
|
return ['success' => false, 'message' => 'Magento 1 entity type not found'];
|
|
}
|
|
|
|
// Get M1 attribute group
|
|
$m1Group = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute_group')
|
|
->where('attribute_group_id', $m1GroupId)
|
|
->where('attribute_set_id', $m1SetId)
|
|
->first();
|
|
|
|
if (!$m1Group) {
|
|
return ['success' => false, 'message' => 'Attribute group not found in Magento 1'];
|
|
}
|
|
|
|
// Get M1 attribute set
|
|
$m1Set = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute_set')
|
|
->where('attribute_set_id', $m1SetId)
|
|
->where('entity_type_id', $m1EntityTypeId)
|
|
->first();
|
|
|
|
if (!$m1Set) {
|
|
return ['success' => false, 'message' => 'Attribute set not found in Magento 1'];
|
|
}
|
|
|
|
// Get M2 entity type ID
|
|
$m2EntityTypeId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_type')
|
|
->where('entity_type_code', 'catalog_category')
|
|
->value('entity_type_id');
|
|
|
|
if (!$m2EntityTypeId) {
|
|
return ['success' => false, 'message' => 'Magento 2 entity type not found'];
|
|
}
|
|
|
|
// Find or create the corresponding attribute set in M2
|
|
$m2Set = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_set')
|
|
->where('entity_type_id', $m2EntityTypeId)
|
|
->where('attribute_set_name', $m1Set->attribute_set_name)
|
|
->first();
|
|
|
|
if (!$m2Set) {
|
|
// Create the attribute set in M2
|
|
$m2SetId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_set')
|
|
->insertGetId([
|
|
'entity_type_id' => $m2EntityTypeId,
|
|
'attribute_set_name' => $m1Set->attribute_set_name,
|
|
'sort_order' => $m1Set->sort_order ?? 0,
|
|
]);
|
|
$m2Set = (object)['attribute_set_id' => $m2SetId];
|
|
} else {
|
|
$m2SetId = $m2Set->attribute_set_id;
|
|
}
|
|
|
|
// Check if group already exists in M2
|
|
$existingGroup = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_group')
|
|
->where('attribute_set_id', $m2SetId)
|
|
->where('attribute_group_name', $m1Group->attribute_group_name)
|
|
->first();
|
|
|
|
if ($existingGroup) {
|
|
return ['success' => false, 'message' => 'Attribute group already exists in Magento 2'];
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
// Create the attribute group in M2
|
|
$m2GroupId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute_group')
|
|
->insertGetId([
|
|
'attribute_set_id' => $m2SetId,
|
|
'attribute_group_name' => $m1Group->attribute_group_name,
|
|
'sort_order' => $m1Group->sort_order ?? 0,
|
|
]);
|
|
|
|
// Get attributes from M1 group
|
|
$m1Attributes = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_entity_attribute')
|
|
->where('attribute_set_id', $m1SetId)
|
|
->where('attribute_group_id', $m1GroupId)
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
// Migrate attributes to M2 group (if they exist in M2)
|
|
$migratedCount = 0;
|
|
foreach ($m1Attributes as $m1EntityAttr) {
|
|
// Get M1 attribute code
|
|
$m1Attribute = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'eav_attribute')
|
|
->where('attribute_id', $m1EntityAttr->attribute_id)
|
|
->first();
|
|
|
|
if ($m1Attribute) {
|
|
// Find corresponding M2 attribute by code
|
|
$m2Attribute = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_attribute')
|
|
->where('entity_type_id', $m2EntityTypeId)
|
|
->where('attribute_code', $m1Attribute->attribute_code)
|
|
->first();
|
|
|
|
if ($m2Attribute) {
|
|
// Check if already in entity_attribute
|
|
$exists = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_attribute')
|
|
->where('attribute_set_id', $m2SetId)
|
|
->where('attribute_group_id', $m2GroupId)
|
|
->where('attribute_id', $m2Attribute->attribute_id)
|
|
->exists();
|
|
|
|
if (!$exists) {
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'eav_entity_attribute')
|
|
->insert([
|
|
'entity_type_id' => $m2EntityTypeId,
|
|
'attribute_set_id' => $m2SetId,
|
|
'attribute_group_id' => $m2GroupId,
|
|
'attribute_id' => $m2Attribute->attribute_id,
|
|
'sort_order' => $m1EntityAttr->sort_order ?? 0,
|
|
]);
|
|
$migratedCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Attribute group migrated successfully',
|
|
'group_name' => $m1Group->attribute_group_name,
|
|
'm2_group_id' => $m2GroupId,
|
|
'attributes_migrated' => $migratedCount,
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
if (isset($this->magento2Connection)) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
}
|
|
Log::error("Error migrating attribute group {$m1GroupId}: " . $e->getMessage());
|
|
return ['success' => false, 'message' => 'Failed to migrate attribute group: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all products from Magento 1
|
|
*/
|
|
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)
|
|
{
|
|
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();
|
|
|
|
// 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;
|
|
}
|
|
|
|
foreach ($m1Products as $m1Product) {
|
|
try {
|
|
$m1Sku = $m1Product->sku ?? 'N/A';
|
|
$hasSku = ($m1Sku !== 'N/A' && !empty($m1Sku));
|
|
|
|
// 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();
|
|
}
|
|
|
|
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
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
if (!$dryRun && isset($this->magento2Connection)) {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
}
|
|
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' => []
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
public function getStoreMapping()
|
|
{
|
|
// If store mapping was set during category migration, use it
|
|
if (!empty($this->storeMapping)) {
|
|
return $this->storeMapping;
|
|
}
|
|
|
|
// 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++;
|
|
Log::error("Error migrating product option M1 ID {$m1OptionId}: " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
Log::error("Error migrating catalog_product_option tables: " . $e->getMessage());
|
|
$errorCount++;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get category mapping from previous migrations
|
|
*/
|
|
public function getCategoryMapping()
|
|
{
|
|
// Try to get mapping from category_mapping table or build it from existing categories
|
|
// Build it by matching category names (case-insensitive, ignoring parent differences)
|
|
$mapping = [];
|
|
|
|
try {
|
|
$m1Categories = $this->getMagento1Categories();
|
|
$m2Categories = $this->getMagento2Categories();
|
|
|
|
// Create a map of M2 categories by normalized name (case-insensitive)
|
|
// Use the first match if multiple categories have the same name
|
|
$m2CategoryMap = [];
|
|
foreach ($m2Categories as $m2Cat) {
|
|
$normalizedName = strtolower(trim($m2Cat->name ?? ''));
|
|
if (!empty($normalizedName) && !isset($m2CategoryMap[$normalizedName])) {
|
|
$m2CategoryMap[$normalizedName] = $m2Cat->entity_id;
|
|
}
|
|
}
|
|
|
|
// Match M1 categories to M2 by normalized name
|
|
foreach ($m1Categories as $m1Cat) {
|
|
$normalizedName = strtolower(trim($m1Cat->name ?? ''));
|
|
if (!empty($normalizedName) && isset($m2CategoryMap[$normalizedName])) {
|
|
$mapping[$m1Cat->entity_id] = $m2CategoryMap[$normalizedName];
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('Error building category mapping: ' . $e->getMessage());
|
|
}
|
|
|
|
return $mapping;
|
|
}
|
|
|
|
/**
|
|
* 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, $progressKey = null)
|
|
{
|
|
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();
|
|
$totalCustomers = $m1Customers->count();
|
|
|
|
// Initialize progress tracking
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalCustomers,
|
|
'current' => 0,
|
|
'added' => 0,
|
|
'updated' => 0,
|
|
'errors' => 0,
|
|
'status' => 'running',
|
|
'current_email' => ''
|
|
], 3600);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
$currentIndex = 0;
|
|
foreach ($m1Customers as $m1Customer) {
|
|
$currentIndex++;
|
|
try {
|
|
$m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null;
|
|
|
|
// Update progress if tracking enabled
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalCustomers,
|
|
'current' => $currentIndex,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'running',
|
|
'current_email' => $m1Email ?? 'N/A'
|
|
], 3600);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Update progress to completed
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalCustomers,
|
|
'current' => $totalCustomers,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'completed',
|
|
'current_email' => ''
|
|
], 3600);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Update progress to failed
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => isset($totalCustomers) ? $totalCustomers : 0,
|
|
'current' => isset($currentIndex) ? $currentIndex : 0,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'failed',
|
|
'current_email' => ''
|
|
], 3600);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all orders from Magento 1
|
|
*/
|
|
public function getMagento1Orders()
|
|
{
|
|
try {
|
|
$orders = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order')
|
|
->select('entity_id', 'increment_id', 'customer_email', 'status', 'grand_total', 'created_at', 'updated_at')
|
|
->orderBy('entity_id')
|
|
->get();
|
|
|
|
return $orders;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 1 orders: ' . $e->getMessage());
|
|
Log::error('Stack trace: ' . $e->getTraceAsString());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all orders from Magento 2
|
|
*/
|
|
public function getMagento2Orders()
|
|
{
|
|
try {
|
|
$orders = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->select('entity_id', 'increment_id', 'customer_email', 'status', 'grand_total', 'created_at', 'updated_at')
|
|
->orderBy('entity_id')
|
|
->get();
|
|
|
|
return $orders;
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching Magento 2 orders: ' . $e->getMessage());
|
|
Log::error('Stack trace: ' . $e->getTraceAsString());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Magento 1 orders that don't exist in Magento 2
|
|
*/
|
|
public function getM1OrdersNotInM2()
|
|
{
|
|
try {
|
|
$m1Orders = $this->getMagento1Orders();
|
|
$m2Orders = $this->getMagento2Orders();
|
|
|
|
// Get all M2 increment IDs - create a set for faster lookup
|
|
$m2IncrementIdsSet = [];
|
|
foreach ($m2Orders as $m2Order) {
|
|
$incrementId = $m2Order->increment_id ?? null;
|
|
if (!empty($incrementId) && is_string($incrementId)) {
|
|
$normalizedIncrementId = trim($incrementId);
|
|
if (!empty($normalizedIncrementId)) {
|
|
$m2IncrementIdsSet[$normalizedIncrementId] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter M1 orders that don't exist in M2
|
|
$missingOrders = collect();
|
|
foreach ($m1Orders as $m1Order) {
|
|
$incrementId = $m1Order->increment_id ?? null;
|
|
if (!empty($incrementId) && is_string($incrementId)) {
|
|
$normalizedIncrementId = trim($incrementId);
|
|
if (!empty($normalizedIncrementId) && !isset($m2IncrementIdsSet[$normalizedIncrementId])) {
|
|
$missingOrders->push($m1Order);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $missingOrders->values();
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching missing orders: ' . $e->getMessage());
|
|
Log::error('Stack trace: ' . $e->getTraceAsString());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Magento 2 orders that don't exist in Magento 1
|
|
*/
|
|
public function getM2OrdersNotInM1()
|
|
{
|
|
try {
|
|
$m1Orders = $this->getMagento1Orders();
|
|
$m2Orders = $this->getMagento2Orders();
|
|
|
|
// Get all M1 increment IDs - create a set for faster lookup
|
|
$m1IncrementIdsSet = [];
|
|
foreach ($m1Orders as $m1Order) {
|
|
$incrementId = $m1Order->increment_id ?? null;
|
|
if (!empty($incrementId) && is_string($incrementId)) {
|
|
$normalizedIncrementId = trim($incrementId);
|
|
if (!empty($normalizedIncrementId)) {
|
|
$m1IncrementIdsSet[$normalizedIncrementId] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter M2 orders that don't exist in M1
|
|
$missingOrders = collect();
|
|
foreach ($m2Orders as $m2Order) {
|
|
$incrementId = $m2Order->increment_id ?? null;
|
|
if (!empty($incrementId) && is_string($incrementId)) {
|
|
$normalizedIncrementId = trim($incrementId);
|
|
if (!empty($normalizedIncrementId) && !isset($m1IncrementIdsSet[$normalizedIncrementId])) {
|
|
$missingOrders->push($m2Order);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $missingOrders->values();
|
|
} catch (Exception $e) {
|
|
Log::error('Error fetching M2 orders not in M1: ' . $e->getMessage());
|
|
Log::error('Stack trace: ' . $e->getTraceAsString());
|
|
return collect([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build an M1 -> M2 column map for a table pair, using the columns that exist
|
|
* in both schemas plus a fixed list of known M1 -> M2 renames (e.g. M1's
|
|
* hidden_tax_* columns became M2's discount_tax_compensation_* columns).
|
|
*/
|
|
protected function getMappedColumns($m1Table, $m2Table)
|
|
{
|
|
$cacheKey = $m1Table . '=>' . $m2Table;
|
|
if (isset($this->columnMapCache[$cacheKey])) {
|
|
return $this->columnMapCache[$cacheKey];
|
|
}
|
|
|
|
$m1Cols = DB::connection($this->magento1Connection)
|
|
->getSchemaBuilder()
|
|
->getColumnListing($this->magento1Prefix . $m1Table);
|
|
$m2Cols = DB::connection($this->magento2Connection)
|
|
->getSchemaBuilder()
|
|
->getColumnListing($this->magento2Prefix . $m2Table);
|
|
$m2ColSet = array_flip($m2Cols);
|
|
|
|
$renames = [
|
|
'hidden_tax_amount' => 'discount_tax_compensation_amount',
|
|
'base_hidden_tax_amount' => 'base_discount_tax_compensation_amount',
|
|
'shipping_hidden_tax_amount' => 'shipping_discount_tax_compensation_amount',
|
|
'base_shipping_hidden_tax_amnt' => 'base_shipping_discount_tax_compensation_amnt',
|
|
'hidden_tax_invoiced' => 'discount_tax_compensation_invoiced',
|
|
'base_hidden_tax_invoiced' => 'base_discount_tax_compensation_invoiced',
|
|
'hidden_tax_refunded' => 'discount_tax_compensation_refunded',
|
|
'base_hidden_tax_refunded' => 'base_discount_tax_compensation_refunded',
|
|
'hidden_tax_canceled' => 'discount_tax_compensation_canceled',
|
|
'cc_last4' => 'cc_last_4',
|
|
];
|
|
|
|
$map = [];
|
|
foreach ($m1Cols as $col) {
|
|
if (isset($m2ColSet[$col])) {
|
|
$map[$col] = $col;
|
|
} elseif (isset($renames[$col]) && isset($m2ColSet[$renames[$col]])) {
|
|
$map[$col] = $renames[$col];
|
|
}
|
|
}
|
|
|
|
return $this->columnMapCache[$cacheKey] = $map;
|
|
}
|
|
|
|
/**
|
|
* Project an M1 row object onto its M2 column names using the supplied map.
|
|
*/
|
|
protected function buildMappedRow($m1Row, array $colMap)
|
|
{
|
|
$out = [];
|
|
foreach ($colMap as $m1Col => $m2Col) {
|
|
if (property_exists($m1Row, $m1Col)) {
|
|
$out[$m2Col] = $m1Row->$m1Col;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Populate sales_order_grid for the given M2 order id. Magento normally fills
|
|
* this via the order_grid indexer; we write to it directly so the order shows
|
|
* up in the admin grid without needing bin/magento indexer:reindex.
|
|
*/
|
|
protected function populateOrderGrid($m2OrderId)
|
|
{
|
|
$conn = DB::connection($this->magento2Connection);
|
|
|
|
$order = $conn->table($this->magento2Prefix . 'sales_order')
|
|
->where('entity_id', $m2OrderId)->first();
|
|
if (!$order) {
|
|
return;
|
|
}
|
|
|
|
$billing = $conn->table($this->magento2Prefix . 'sales_order_address')
|
|
->where('parent_id', $m2OrderId)
|
|
->where('address_type', 'billing')
|
|
->first();
|
|
$shipping = $conn->table($this->magento2Prefix . 'sales_order_address')
|
|
->where('parent_id', $m2OrderId)
|
|
->where('address_type', 'shipping')
|
|
->first();
|
|
$payment = $conn->table($this->magento2Prefix . 'sales_order_payment')
|
|
->where('parent_id', $m2OrderId)
|
|
->first();
|
|
|
|
$name = function ($a) {
|
|
if (!$a) return null;
|
|
return trim(($a->firstname ?? '') . ' ' . ($a->lastname ?? ''));
|
|
};
|
|
$address = function ($a) {
|
|
if (!$a) return null;
|
|
return trim(implode(', ', array_filter([
|
|
$a->street ?? null,
|
|
$a->city ?? null,
|
|
$a->region ?? null,
|
|
$a->postcode ?? null,
|
|
$a->country_id ?? null,
|
|
])));
|
|
};
|
|
|
|
$conn->table($this->magento2Prefix . 'sales_order_grid')
|
|
->updateOrInsert(
|
|
['entity_id' => $m2OrderId],
|
|
[
|
|
'status' => $order->status,
|
|
'store_id' => $order->store_id,
|
|
'store_name' => $order->store_name,
|
|
'customer_id' => $order->customer_id,
|
|
'base_grand_total' => $order->base_grand_total,
|
|
'base_total_paid' => $order->base_total_paid,
|
|
'grand_total' => $order->grand_total,
|
|
'total_paid' => $order->total_paid,
|
|
'increment_id' => $order->increment_id,
|
|
'base_currency_code' => $order->base_currency_code,
|
|
'order_currency_code' => $order->order_currency_code,
|
|
'shipping_name' => $name($shipping),
|
|
'billing_name' => $name($billing),
|
|
'created_at' => $order->created_at,
|
|
'updated_at' => $order->updated_at,
|
|
'billing_address' => $address($billing),
|
|
'shipping_address' => $address($shipping),
|
|
'shipping_information' => $order->shipping_description,
|
|
'customer_email' => $order->customer_email,
|
|
'subtotal' => $order->subtotal,
|
|
'shipping_and_handling' => $order->shipping_amount,
|
|
'customer_name' => trim(($order->customer_firstname ?? '') . ' ' . ($order->customer_lastname ?? '')),
|
|
'payment_method' => $payment->method ?? null,
|
|
'total_refunded' => $order->total_refunded,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Migrate all orders from Magento 1 to Magento 2 (entity + items + addresses
|
|
* + payment + status history + grid).
|
|
*/
|
|
public function migrateOrders($dryRun = false, $progressKey = null)
|
|
{
|
|
try {
|
|
$this->migrationLog = [];
|
|
$addedCount = 0;
|
|
$updatedCount = 0;
|
|
$errorCount = 0;
|
|
$currentIndex = 0;
|
|
$totalOrders = 0;
|
|
|
|
$orderColMap = $this->getMappedColumns('sales_flat_order', 'sales_order');
|
|
$itemColMap = $this->getMappedColumns('sales_flat_order_item', 'sales_order_item');
|
|
$addressColMap = $this->getMappedColumns('sales_flat_order_address', 'sales_order_address');
|
|
$paymentColMap = $this->getMappedColumns('sales_flat_order_payment', 'sales_order_payment');
|
|
$historyColMap = $this->getMappedColumns('sales_flat_order_status_history', 'sales_order_status_history');
|
|
|
|
$m1Orders = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order')
|
|
->orderBy('entity_id')
|
|
->get();
|
|
$totalOrders = $m1Orders->count();
|
|
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalOrders,
|
|
'current' => 0,
|
|
'added' => 0,
|
|
'updated' => 0,
|
|
'errors' => 0,
|
|
'status' => 'running',
|
|
'current_increment_id' => ''
|
|
], 3600);
|
|
}
|
|
|
|
foreach ($m1Orders as $m1Order) {
|
|
$currentIndex++;
|
|
$m1OrderId = $m1Order->entity_id;
|
|
$m1IncrementId = !empty($m1Order->increment_id) ? trim($m1Order->increment_id) : null;
|
|
|
|
try {
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalOrders,
|
|
'current' => $currentIndex,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'running',
|
|
'current_increment_id' => $m1IncrementId ?? 'N/A',
|
|
], 3600);
|
|
}
|
|
|
|
if (empty($m1IncrementId)) {
|
|
$this->migrationLog[] = "SKIPPED: M1 order ID {$m1OrderId} - no increment_id";
|
|
continue;
|
|
}
|
|
|
|
$m2Existing = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->where('increment_id', $m1IncrementId)
|
|
->first();
|
|
$isNew = !$m2Existing;
|
|
$m2OrderId = $m2Existing->entity_id ?? null;
|
|
|
|
if ($dryRun) {
|
|
$this->migrationLog[] = $isNew
|
|
? "Would add new order: {$m1IncrementId}"
|
|
: "Would update existing order: {$m1IncrementId} (ID: {$m2OrderId})";
|
|
$isNew ? $addedCount++ : $updatedCount++;
|
|
continue;
|
|
}
|
|
|
|
// Look up M2 customer by email - M1 customer ids do not match M2.
|
|
$m2CustomerId = null;
|
|
if (!empty($m1Order->customer_email)) {
|
|
$m2Customer = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'customer_entity')
|
|
->where('email', $m1Order->customer_email)
|
|
->first();
|
|
$m2CustomerId = $m2Customer ? $m2Customer->entity_id : null;
|
|
}
|
|
|
|
$orderData = $this->buildMappedRow($m1Order, $orderColMap);
|
|
unset(
|
|
$orderData['entity_id'],
|
|
$orderData['billing_address_id'],
|
|
$orderData['shipping_address_id']
|
|
);
|
|
$orderData['customer_id'] = $m2CustomerId;
|
|
|
|
DB::connection($this->magento2Connection)->beginTransaction();
|
|
|
|
if ($isNew) {
|
|
$m2OrderId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->insertGetId($orderData);
|
|
$addedCount++;
|
|
} else {
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->where('entity_id', $m2OrderId)
|
|
->update($orderData);
|
|
|
|
// Wipe related rows so the re-import is deterministic.
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_item')
|
|
->where('order_id', $m2OrderId)->delete();
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_address')
|
|
->where('parent_id', $m2OrderId)->delete();
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_payment')
|
|
->where('parent_id', $m2OrderId)->delete();
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_status_history')
|
|
->where('parent_id', $m2OrderId)->delete();
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_grid')
|
|
->where('entity_id', $m2OrderId)->delete();
|
|
$updatedCount++;
|
|
}
|
|
|
|
// Addresses
|
|
$billingAddressId = null;
|
|
$shippingAddressId = null;
|
|
$m1Addresses = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order_address')
|
|
->where('parent_id', $m1OrderId)
|
|
->get();
|
|
foreach ($m1Addresses as $m1Addr) {
|
|
$addrData = $this->buildMappedRow($m1Addr, $addressColMap);
|
|
unset($addrData['entity_id']);
|
|
$addrData['parent_id'] = $m2OrderId;
|
|
$newAddrId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_address')
|
|
->insertGetId($addrData);
|
|
if (($m1Addr->address_type ?? null) === 'billing') {
|
|
$billingAddressId = $newAddrId;
|
|
} elseif (($m1Addr->address_type ?? null) === 'shipping') {
|
|
$shippingAddressId = $newAddrId;
|
|
}
|
|
}
|
|
if ($billingAddressId || $shippingAddressId) {
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->where('entity_id', $m2OrderId)
|
|
->update(array_filter([
|
|
'billing_address_id' => $billingAddressId,
|
|
'shipping_address_id' => $shippingAddressId,
|
|
]));
|
|
}
|
|
|
|
// Items (two-pass so parent_item_id can be remapped to new ids)
|
|
$m1Items = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order_item')
|
|
->where('order_id', $m1OrderId)
|
|
->get();
|
|
$itemIdMap = [];
|
|
foreach ($m1Items as $m1Item) {
|
|
$itemData = $this->buildMappedRow($m1Item, $itemColMap);
|
|
unset($itemData['item_id']);
|
|
$itemData['order_id'] = $m2OrderId;
|
|
$itemData['parent_item_id'] = null;
|
|
$newItemId = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_item')
|
|
->insertGetId($itemData);
|
|
$itemIdMap[$m1Item->item_id] = $newItemId;
|
|
}
|
|
foreach ($m1Items as $m1Item) {
|
|
if (!empty($m1Item->parent_item_id) && isset($itemIdMap[$m1Item->parent_item_id])) {
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_item')
|
|
->where('item_id', $itemIdMap[$m1Item->item_id])
|
|
->update(['parent_item_id' => $itemIdMap[$m1Item->parent_item_id]]);
|
|
}
|
|
}
|
|
|
|
// Payment
|
|
$m1Payment = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order_payment')
|
|
->where('parent_id', $m1OrderId)
|
|
->first();
|
|
if ($m1Payment) {
|
|
$payData = $this->buildMappedRow($m1Payment, $paymentColMap);
|
|
unset($payData['entity_id']);
|
|
$payData['parent_id'] = $m2OrderId;
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_payment')
|
|
->insert($payData);
|
|
}
|
|
|
|
// Status history
|
|
$m1History = DB::connection($this->magento1Connection)
|
|
->table($this->magento1Prefix . 'sales_flat_order_status_history')
|
|
->where('parent_id', $m1OrderId)
|
|
->get();
|
|
foreach ($m1History as $m1Hist) {
|
|
$histData = $this->buildMappedRow($m1Hist, $historyColMap);
|
|
unset($histData['entity_id']);
|
|
$histData['parent_id'] = $m2OrderId;
|
|
if (empty($histData['entity_name'])) {
|
|
$histData['entity_name'] = 'order';
|
|
}
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order_status_history')
|
|
->insert($histData);
|
|
}
|
|
|
|
// sales_order_grid (Magento normally fills this from the indexer;
|
|
// populate directly so the order is visible in the admin grid).
|
|
$this->populateOrderGrid($m2OrderId);
|
|
|
|
DB::connection($this->magento2Connection)->commit();
|
|
|
|
$this->migrationLog[] = $isNew
|
|
? "Added new order: {$m1IncrementId} (ID: {$m2OrderId})"
|
|
: "Updated existing order: {$m1IncrementId} (ID: {$m2OrderId})";
|
|
|
|
} catch (Exception $e) {
|
|
try {
|
|
DB::connection($this->magento2Connection)->rollBack();
|
|
} catch (Exception $rollbackEx) {
|
|
// Ignore - no active transaction
|
|
}
|
|
$errorCount++;
|
|
$this->migrationLog[] = "ERROR: Failed to migrate order " . ($m1IncrementId ?? 'N/A') . ": " . $e->getMessage();
|
|
Log::error("Error migrating order " . ($m1IncrementId ?? 'N/A') . ": " . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => $totalOrders,
|
|
'current' => $totalOrders,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'completed',
|
|
'current_increment_id' => '',
|
|
], 3600);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => $dryRun ? 'Dry run completed' : 'Order migration completed',
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'log' => $this->migrationLog,
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
|
|
// Update progress to failed
|
|
if ($progressKey && !$dryRun) {
|
|
Cache::put($progressKey, [
|
|
'total' => isset($totalOrders) ? $totalOrders : 0,
|
|
'current' => isset($currentIndex) ? $currentIndex : 0,
|
|
'added' => $addedCount,
|
|
'updated' => $updatedCount,
|
|
'errors' => $errorCount,
|
|
'status' => 'failed',
|
|
'current_increment_id' => ''
|
|
], 3600);
|
|
}
|
|
|
|
Log::error('Error migrating orders: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Migration failed: ' . $e->getMessage(),
|
|
'added' => 0,
|
|
'updated' => 0,
|
|
'errors' => 0,
|
|
'log' => []
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a single order from Magento 2
|
|
*/
|
|
public function deleteM2Order($orderId)
|
|
{
|
|
try {
|
|
$order = DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->where('entity_id', $orderId)
|
|
->first();
|
|
|
|
if (!$order) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Order not found'
|
|
];
|
|
}
|
|
|
|
$incrementId = $order->increment_id ?? 'N/A';
|
|
|
|
// Delete order (cascade deletes should handle related records)
|
|
DB::connection($this->magento2Connection)
|
|
->table($this->magento2Prefix . 'sales_order')
|
|
->where('entity_id', $orderId)
|
|
->delete();
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "Order {$incrementId} deleted successfully"
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
Log::error('Error deleting M2 order: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Deletion failed: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|