migrate/app/Services/MagentoCategoryMigrationSer...

4615 lines
192 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
class MagentoCategoryMigrationService
{
protected $magento1Connection;
protected $magento2Connection;
protected $magento1Prefix;
protected $magento2Prefix;
protected $storeMapping = [];
protected $categoryMapping = [];
protected $migrationLog = [];
protected $addedCount = 0;
protected $existingCount = 0;
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', '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', '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
*/
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) {
$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',
'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',
'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',
'created_at' => $m1Product->created_at ?? now(),
'updated_at' => now(),
];
// If SKU column exists, set it to NULL or empty
if ($m2HasSkuColumn) {
$insertData['sku'] = null;
}
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity')
->insert($insertData);
$this->migrationLog[] = "Created new product: No SKU (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) {
$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);
}
} 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;
}
}
}
}
}
}
}
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
*/
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
*/
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');
if (!$m1MediaGalleryAttrId) {
// Media gallery attribute doesn't exist in M1, skip
return;
}
// Get media_gallery attribute ID from M2
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->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 = [];
// First, try to get images from M1 media gallery tables (if they exist)
try {
$m1MediaGalleryTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery';
$m1MediaGalleryValueTable = $this->magento1Prefix . 'catalog_product_entity_media_gallery_value';
// Check if M1 media gallery tables exist
$m1GalleryImages = DB::connection($this->magento1Connection)
->table($m1MediaGalleryTable)
->where('entity_id', $m1ProductId)
->get();
if ($m1GalleryImages->isNotEmpty()) {
// M1 has media gallery tables, use them
foreach ($m1GalleryImages as $m1Image) {
$valueId = $m1Image->value_id;
// Get value data
$m1Value = DB::connection($this->magento1Connection)
->table($m1MediaGalleryValueTable)
->where('value_id', $valueId)
->where('store_id', 0)
->first();
$images[] = [
'file' => $m1Image->value,
'label' => $m1Value->label ?? null,
'position' => $m1Value->position ?? 0,
'disabled' => $m1Value->disabled ?? 0,
];
}
}
} catch (Exception $e) {
// M1 media gallery tables don't exist, fall back to varchar table
Log::debug("M1 media gallery tables not found, using varchar table: " . $e->getMessage());
}
// If no images from gallery tables, try varchar table (serialized data)
if (empty($images)) {
$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, skip
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) {
if (is_string($img)) {
$images[] = ['file' => $img];
} elseif (is_array($img)) {
$images[] = [
'file' => $img['file'] ?? $img['value'] ?? null,
'label' => $img['label'] ?? $img['label_default'] ?? null,
'position' => isset($img['position']) ? (int)$img['position'] : 0,
'disabled' => isset($img['disabled']) ? (int)$img['disabled'] : 0,
];
}
}
}
}
}
if (empty($images)) {
return;
}
// 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,
]);
}
// Insert/update catalog_product_entity_media_gallery_value for default store (store_id = 0)
$valueExists = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value')
->where('value_id', $valueId)
->where('store_id', 0)
->exists();
if (!$valueExists) {
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity_media_gallery_value')
->insert([
'value_id' => $valueId,
'store_id' => 0,
'label' => $label,
'position' => $position,
'disabled' => $disabled,
]);
} 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)
->update([
'label' => $label,
'position' => $position,
'disabled' => $disabled,
]);
}
}
} 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;
}
/**
* 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
*/
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,
]);
}
}
}
}
/**
* 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
*/
protected 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)
{
try {
$this->migrationLog = [];
$addedCount = 0;
$updatedCount = 0;
$errorCount = 0;
// Get M1 and M2 entity type IDs
$m1EntityTypeId = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
$m2EntityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if (!$m1EntityTypeId || !$m2EntityTypeId) {
return [
'success' => false,
'message' => 'Entity type not found',
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => []
];
}
// Get all M1 customers
$m1Customers = $this->getMagento1Customers();
// Get all customer attribute IDs from M1
$m1AttributeIds = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $m1EntityTypeId)
->pluck('attribute_id', 'attribute_code')
->toArray();
// Get all customer attribute IDs from M2
$m2AttributeIds = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $m2EntityTypeId)
->pluck('attribute_id', 'attribute_code')
->toArray();
if (!$dryRun) {
DB::connection($this->magento2Connection)->beginTransaction();
}
foreach ($m1Customers as $m1Customer) {
try {
$m1Email = !empty($m1Customer->email) ? strtolower(trim($m1Customer->email)) : null;
if (empty($m1Email)) {
$this->migrationLog[] = "SKIPPED: Customer ID {$m1Customer->entity_id} - no email address";
continue;
}
// Check if customer exists in M2 by email (case-insensitive)
$m2Customer = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->whereRaw('LOWER(TRIM(email)) = ?', [strtolower(trim($m1Email))])
->first();
$m2CustomerId = null;
$isNew = false;
if ($m2Customer) {
// Customer exists, update
$m2CustomerId = $m2Customer->entity_id;
if (!$dryRun) {
$this->migrationLog[] = "Updating existing customer: {$m1Email} (ID: {$m2CustomerId})";
} else {
$this->migrationLog[] = "Would update existing customer: {$m1Email} (ID: {$m2CustomerId})";
}
$updatedCount++;
} else {
// Customer doesn't exist, create
if (!$dryRun) {
// Insert customer entity
$m2CustomerId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->insertGetId([
'email' => $m1Email,
'website_id' => $m1Customer->website_id ?? 1,
'group_id' => $m1Customer->group_id ?? 1,
'created_at' => $m1Customer->created_at ?? now(),
'updated_at' => $m1Customer->updated_at ?? now(),
]);
$this->migrationLog[] = "Added new customer: {$m1Email} (ID: {$m2CustomerId})";
} else {
$this->migrationLog[] = "Would add new customer: {$m1Email}";
$m2CustomerId = 0; // Placeholder for dry run
}
$addedCount++;
$isNew = true;
}
if (!$dryRun && $m2CustomerId) {
// Migrate customer attributes
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$m1Table = $this->magento1Prefix . 'customer_entity_' . $tableType;
$m2Table = $this->magento2Prefix . 'customer_entity_' . $tableType;
try {
// Get all attributes for this customer from M1
$m1Attributes = DB::connection($this->magento1Connection)
->table($m1Table)
->where('entity_id', $m1Customer->entity_id)
->get();
foreach ($m1Attributes as $m1Attr) {
// Check if attribute exists in M2
$attributeCode = array_search($m1Attr->attribute_id, $m1AttributeIds);
if ($attributeCode && isset($m2AttributeIds[$attributeCode])) {
$m2AttributeId = $m2AttributeIds[$attributeCode];
// Check if attribute value already exists (customer attributes don't have store_id)
$exists = DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2CustomerId)
->where('attribute_id', $m2AttributeId)
->exists();
if (!$exists) {
// Customer attributes don't have store_id column
$insertData = [
'entity_id' => $m2CustomerId,
'attribute_id' => $m2AttributeId,
'value' => $m1Attr->value ?? null,
];
DB::connection($this->magento2Connection)
->table($m2Table)
->insert($insertData);
} else {
// Update existing attribute
DB::connection($this->magento2Connection)
->table($m2Table)
->where('entity_id', $m2CustomerId)
->where('attribute_id', $m2AttributeId)
->update(['value' => $m1Attr->value ?? null]);
}
}
}
} catch (Exception $e) {
// Table might not exist, continue
Log::warning("Table {$m1Table} or {$m2Table} might not exist: " . $e->getMessage());
}
}
}
} catch (Exception $e) {
$errorCount++;
$m1Email = $m1Customer->email ?? 'N/A';
$this->migrationLog[] = "ERROR: Failed to migrate customer {$m1Email}: " . $e->getMessage();
Log::error("Error migrating customer {$m1Email}: " . $e->getMessage());
}
}
if (!$dryRun) {
DB::connection($this->magento2Connection)->commit();
}
return [
'success' => true,
'message' => $dryRun ? 'Dry run completed' : 'Customer migration completed',
'added' => $addedCount,
'updated' => $updatedCount,
'errors' => $errorCount,
'log' => $this->migrationLog
];
} catch (Exception $e) {
if (!$dryRun) {
DB::connection($this->magento2Connection)->rollBack();
}
Log::error('Error migrating customers: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage(),
'added' => 0,
'updated' => 0,
'errors' => 0,
'log' => []
];
}
}
/**
* Delete a single customer from Magento 2
*/
public function deleteM2Customer($customerId)
{
try {
DB::connection($this->magento2Connection)->beginTransaction();
// Get entity type ID
$entityTypeId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'customer')
->value('entity_type_id');
if ($entityTypeId) {
// Delete attribute values from all attribute tables
$attributeTables = ['varchar', 'int', 'text', 'decimal', 'datetime'];
foreach ($attributeTables as $tableType) {
$table = $this->magento2Prefix . 'customer_entity_' . $tableType;
try {
DB::connection($this->magento2Connection)
->table($table)
->where('entity_id', $customerId)
->delete();
} catch (Exception $e) {
Log::warning("Table {$table} might not exist: " . $e->getMessage());
}
}
}
// Delete the customer entity
DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'customer_entity')
->where('entity_id', $customerId)
->delete();
DB::connection($this->magento2Connection)->commit();
return [
'success' => true,
'message' => 'Customer deleted successfully'
];
} catch (Exception $e) {
DB::connection($this->magento2Connection)->rollBack();
Log::error("Error deleting customer {$customerId}: " . $e->getMessage());
return [
'success' => false,
'message' => 'Failed to delete customer: ' . $e->getMessage()
];
}
}
}