migrate/app/Services/MagentoCategoryMigrationSer...

838 lines
30 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([]);
}
}
/**
* Build hierarchical category tree structure
*/
public function buildCategoryTreeHierarchy($categories)
{
$categoryMap = [];
$rootCategories = [];
// First pass: create a map of all categories with children array
foreach ($categories as $category) {
$categoryMap[$category->entity_id] = [
'id' => $category->entity_id,
'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 ?? '',
'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;
}
/**
* 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 from Magento 2
* Only allows deletion of root-level categories (parent_id = 0 or 1) with no children
*/
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',
];
}
// Check if category is root level (parent_id = 0 or 1)
if ($category->parent_id != 0 && $category->parent_id != 1) {
return [
'success' => false,
'message' => 'Only root-level categories (parent_id = 0 or 1) can be deleted',
];
}
// Check if category has children
$childrenCount = DB::connection($connection)
->table($prefix . 'catalog_category_entity')
->where('parent_id', $categoryId)
->count();
if ($childrenCount > 0) {
return [
'success' => false,
'message' => 'Cannot delete category with subcategories',
];
}
DB::connection($connection)->beginTransaction();
// 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) {
// Get all attribute IDs for this entity type
$attributeIds = DB::connection($connection)
->table($prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeId)
->pluck('attribute_id')
->toArray();
// 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 the category entity
DB::connection($connection)
->table($prefix . 'catalog_category_entity')
->where('entity_id', $categoryId)
->delete();
// Update parent's children_count if parent exists
if ($category->parent_id > 0) {
$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 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;
}
}