migrate/app/Http/Controllers/AdditionalController.php

627 lines
28 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class AdditionalController extends Controller
{
protected $magento1Connection = 'magento1';
protected $magento2Connection = 'magento2';
protected $magento1Prefix;
protected $magento2Prefix;
/** Default attribute code for frontpage tabs (can be overridden in request) */
protected $frontpageTabsAttributeCode = 'frontpage_tabs';
public function __construct()
{
$this->magento1Prefix = config('database.connections.magento1.prefix', '');
$this->magento2Prefix = config('database.connections.magento2.prefix', '');
}
/**
* Compare M1 text attributes (potential tab content) against M2 mgs_protabs config.
* Returns per-scope breakdown of what is configured and what is missing.
*/
public function compareProtabs(Request $request)
{
try {
// --- M2: gather all Protabs entries and website/store names ---
$m2Tabs = DB::connection($this->magento2Connection)
->table('mgs_protabs')
->orderBy('scope')
->orderBy('scope_id')
->orderBy('position')
->get();
$m2Websites = DB::connection($this->magento2Connection)
->table('store_website')
->whereNotIn('website_id', [0])
->whereRaw("name NOT LIKE '%Admin%'")
->pluck('name', 'website_id');
$m2Stores = DB::connection($this->magento2Connection)
->table('store')
->where('store_id', '>', 0)
->pluck('name', 'store_id');
// Tabs keyed by "scope:scope_id" for easy lookup
$tabsByScope = [];
foreach ($m2Tabs as $tab) {
$key = $tab->scope . ':' . $tab->scope_id;
$tabsByScope[$key][] = (array) $tab;
}
// --- M1: find all catalog_product text attributes with values ---
$entityTypeIdM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
// Get all text-backend attributes that actually have product values
$m1TextAttrs = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute as a')
->join($this->magento1Prefix . 'catalog_product_entity_text as v', 'a.attribute_id', '=', 'v.attribute_id')
->where('a.entity_type_id', $entityTypeIdM1)
->select('a.attribute_code', 'a.frontend_label', DB::raw('COUNT(v.value_id) as value_count'))
->whereNotNull('v.value')
->where('v.value', '!=', '')
->groupBy('a.attribute_id', 'a.attribute_code', 'a.frontend_label')
->having('value_count', '>', 0)
->orderBy('a.attribute_code')
->get()
->keyBy('attribute_code');
// --- Build per-scope comparison ---
// Collect all unique scopes from existing tabs
$scopes = $m2Tabs->map(fn($t) => ['scope' => $t->scope, 'scope_id' => $t->scope_id])
->unique(fn($s) => $s['scope'] . ':' . $s['scope_id'])
->values();
$comparison = [];
foreach ($scopes as $scope) {
$key = $scope['scope'] . ':' . $scope['scope_id'];
$existing = $tabsByScope[$key] ?? [];
$existingAttrCodes = collect($existing)
->where('tab_type', 'attribute')
->pluck('value')
->filter()
->flip(); // use as set
// Find M1 text attrs with values not in this scope's protabs
$missing = [];
foreach ($m1TextAttrs as $attrCode => $attr) {
if (!isset($existingAttrCodes[$attrCode])) {
$missing[] = [
'attribute_code' => $attrCode,
'frontend_label' => $attr->frontend_label ?? $attrCode,
'value_count' => $attr->value_count,
'suggested_title' => ucwords(str_replace('_', ' ', $attrCode)),
'suggested_pos' => count($existing) + count($missing) + 1,
];
}
}
$scopeLabel = match ($scope['scope']) {
'default' => 'Default (all stores)',
'websites' => 'Website: ' . ($m2Websites[$scope['scope_id']] ?? 'ID ' . $scope['scope_id']),
'stores' => 'Store: ' . ($m2Stores[$scope['scope_id']] ?? 'ID ' . $scope['scope_id']),
default => $scope['scope'] . ' ' . $scope['scope_id'],
};
$comparison[] = [
'scope' => $scope['scope'],
'scope_id' => $scope['scope_id'],
'scope_label' => $scopeLabel,
'existing' => $existing,
'missing' => $missing,
];
}
return response()->json([
'success' => true,
'comparison' => $comparison,
'm1_text_attr_count' => $m1TextAttrs->count(),
]);
} catch (\Throwable $e) {
Log::error('Protabs compare error: ' . $e->getMessage());
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
/**
* Insert one or more missing tabs into M2 mgs_protabs.
* Accepts an array of tab objects: [{title, tab_type, value, position, scope, scope_id}, ...]
*/
public function syncProtabs(Request $request)
{
$tabs = $request->input('tabs', []);
if (empty($tabs)) {
return response()->json(['success' => false, 'message' => 'No tabs provided.'], 400);
}
$inserted = 0;
$skipped = 0;
$errors = 0;
$log = [];
foreach ($tabs as $tab) {
$scope = $tab['scope'] ?? 'default';
$scopeId = (int)($tab['scope_id'] ?? 0);
$value = trim($tab['value'] ?? '');
$title = trim($tab['title'] ?? '');
$tabType = $tab['tab_type'] ?? 'attribute';
$pos = (int)($tab['position'] ?? 99);
if ($value === '' || $title === '') {
$log[] = "Skipped entry with empty value or title.";
$skipped++;
continue;
}
// Check if already exists for this scope
$exists = DB::connection($this->magento2Connection)
->table('mgs_protabs')
->where('scope', $scope)
->where('scope_id', $scopeId)
->where('tab_type', $tabType)
->where('value', $value)
->exists();
if ($exists) {
$log[] = "Tab '{$title}' ({$value}) already exists for {$scope}:{$scopeId}, skipped.";
$skipped++;
continue;
}
try {
DB::connection($this->magento2Connection)
->table('mgs_protabs')
->insert([
'title' => $title,
'tab_type' => $tabType,
'value' => $value,
'position' => $pos,
'scope' => $scope,
'scope_id' => $scopeId,
]);
$inserted++;
$log[] = "Added tab '{$title}' ({$value}) to {$scope}:{$scopeId} at position {$pos}.";
} catch (\Throwable $e) {
$errors++;
$log[] = "Error adding '{$value}': " . $e->getMessage();
Log::error('Protabs sync error: ' . $e->getMessage());
}
}
return response()->json([
'success' => $errors === 0,
'message' => "Protabs sync: {$inserted} added, {$skipped} skipped, {$errors} errors.",
'inserted' => $inserted,
'skipped' => $skipped,
'errors' => $errors,
'log' => $log,
]);
}
/**
* Show the Additional tools page
*/
public function index()
{
$frontpageTabsInfo = $this->getFrontpageTabsAttributeInfo();
return view('additional.index', [
'frontpageTabsInfo' => $frontpageTabsInfo,
]);
}
/**
* Get info about the frontpage tabs attribute in M1 and M2 (for display / discovery)
*/
protected function getFrontpageTabsAttributeInfo()
{
$info = ['m1' => null, 'm2' => null, 'suggested_codes' => []];
try {
$entityTypeIdM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
$entityTypeIdM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeIdM1 || !$entityTypeIdM2) {
return $info;
}
foreach (['frontpage_tabs', 'front_tabs', 'product_tabs'] as $code) {
$m1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM1)
->where('attribute_code', $code)
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label')
->first();
$m2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM2)
->where('attribute_code', $code)
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_label')
->first();
if ($m1) {
$info['m1'] = $m1;
$info['m2'] = $m2;
break;
}
if ($m1 || $m2) {
$info['suggested_codes'][] = $code;
}
}
if (!$info['m1']) {
$suggested = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM1)
->where(function ($q) {
$q->where('attribute_code', 'like', '%frontpage%')
->orWhere('attribute_code', 'like', '%front%tab%')
->orWhere('attribute_code', 'like', '%tab%');
})
->pluck('attribute_code')
->toArray();
$info['suggested_codes'] = array_unique(array_merge($info['suggested_codes'], $suggested));
}
} catch (\Throwable $e) {
Log::warning('Frontpage tabs attribute info: ' . $e->getMessage());
}
return $info;
}
/**
* Return diagnostic info for an attribute code (M1/M2 existence, backend_type, value row counts)
*/
public function attributeDiagnostic(Request $request)
{
$attributeCode = trim((string) $request->input('attribute_code', '')) ?: 'frontpage_tabs';
try {
$entityTypeIdM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
$entityTypeIdM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeIdM1 || !$entityTypeIdM2) {
return response()->json(['success' => false, 'message' => 'Could not resolve entity type.', 'm1' => null, 'm2' => null]);
}
$attrM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM1)
->where('attribute_code', $attributeCode)
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label')
->first();
$attrM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM2)
->where('attribute_code', $attributeCode)
->select('attribute_id', 'attribute_code', 'backend_type', 'frontend_input', 'frontend_label')
->first();
$diagnostic = ['attribute_code' => $attributeCode, 'm1' => null, 'm2' => null];
if ($attrM1) {
$diagnostic['m1'] = (array) $attrM1;
$bt = $attrM1->backend_type ?? 'varchar';
$tables = ['varchar', 'text', 'int', 'decimal', 'datetime'];
$diagnostic['m1']['value_counts_by_table'] = [];
foreach ($tables as $t) {
try {
$c = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity_' . $t)
->where('attribute_id', $attrM1->attribute_id)
->count();
$diagnostic['m1']['value_counts_by_table'][$t] = $c;
} catch (\Throwable $e) {
$diagnostic['m1']['value_counts_by_table'][$t] = 'error';
}
}
}
if ($attrM2) {
$diagnostic['m2'] = (array) $attrM2;
}
return response()->json(['success' => true, 'diagnostic' => $diagnostic]);
} catch (\Throwable $e) {
Log::warning('Attribute diagnostic: ' . $e->getMessage());
return response()->json(['success' => false, 'message' => $e->getMessage(), 'diagnostic' => null], 500);
}
}
/**
* Sync frontpage tabs from Magento 1 to Magento 2 by SKU
* Scans all M1 products by SKU and ensures M2 products have the same frontpage tabs value(s)
*/
public function syncFrontpageTabs(Request $request)
{
$attributeCode = $request->input('attribute_code', $this->frontpageTabsAttributeCode);
$dryRun = $request->boolean('dry_run', false);
try {
$entityTypeIdM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
$entityTypeIdM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_entity_type')
->where('entity_type_code', 'catalog_product')
->value('entity_type_id');
if (!$entityTypeIdM1 || !$entityTypeIdM2) {
return response()->json([
'success' => false,
'message' => 'Could not resolve catalog_product entity type in M1 or M2.',
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
], 400);
}
$attrM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM1)
->where('attribute_code', $attributeCode)
->first();
if (!$attrM1) {
return response()->json([
'success' => false,
'message' => "Attribute '{$attributeCode}' not found in Magento 1.",
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
], 400);
}
$attrM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM2)
->where('attribute_code', $attributeCode)
->first();
if (!$attrM2) {
return response()->json([
'success' => false,
'message' => "Attribute '{$attributeCode}' not found in Magento 2. Create the attribute in M2 first.",
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
], 400);
}
$backendType = $attrM1->backend_type ?? 'varchar';
if (!in_array($backendType, ['varchar', 'text', 'int', 'decimal', 'datetime'], true)) {
$backendType = 'varchar';
}
$backendTypesToTry = ['varchar', 'text', 'int', 'decimal', 'datetime'];
$m1ValueTable = null;
$m2ValueTable = null;
$resolvedBackendType = null;
foreach ($backendTypesToTry as $bt) {
$t1 = $this->magento1Prefix . 'catalog_product_entity_' . $bt;
$count = 0;
try {
$count = DB::connection($this->magento1Connection)
->table($t1)
->where('attribute_id', $attrM1->attribute_id)
->count();
} catch (\Throwable $e) {
continue;
}
if ($count > 0) {
$resolvedBackendType = $bt;
$m1ValueTable = $t1;
$m2ValueTable = $this->magento2Prefix . 'catalog_product_entity_' . $bt;
break;
}
}
if ($m1ValueTable === null) {
$m1ValueTable = $this->magento1Prefix . 'catalog_product_entity_' . $backendType;
$m2ValueTable = $this->magento2Prefix . 'catalog_product_entity_' . $backendType;
$resolvedBackendType = $backendType;
}
$m1ValueRowCount = 0;
try {
$m1ValueRowCount = DB::connection($this->magento1Connection)
->table($m1ValueTable)
->where('attribute_id', $attrM1->attribute_id)
->count();
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => "Magento 1 value table for attribute '{$attributeCode}' (backend_type {$resolvedBackendType}) not found or error: " . $e->getMessage(),
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
'diagnostic' => ['backend_type' => $resolvedBackendType, 'm1_value_table' => $m1ValueTable],
], 400);
}
if ($m1ValueRowCount === 0) {
return response()->json([
'success' => false,
'message' => "No values found in Magento 1 for attribute '{$attributeCode}'. No products have this attribute set in M1. Check that the attribute is assigned to product attribute sets and that products have values saved.",
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
'diagnostic' => [
'backend_type' => $resolvedBackendType,
'm1_value_table' => $m1ValueTable,
'm1_value_row_count' => 0,
],
], 400);
}
$skuAttrIdM1 = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM1)
->where('attribute_code', 'sku')
->value('attribute_id');
$skuAttrIdM2 = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'eav_attribute')
->where('entity_type_id', $entityTypeIdM2)
->where('attribute_code', 'sku')
->value('attribute_id');
if (!$skuAttrIdM1 || !$skuAttrIdM2) {
return response()->json([
'success' => false,
'message' => 'Could not resolve SKU attribute in M1 or M2.',
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
], 400);
}
$m1ProductsWithTabs = DB::connection($this->magento1Connection)
->table($this->magento1Prefix . 'catalog_product_entity as e')
->leftJoin($this->magento1Prefix . 'catalog_product_entity_varchar as sku_v', function ($join) use ($skuAttrIdM1) {
$join->on('e.entity_id', '=', 'sku_v.entity_id')
->where('sku_v.attribute_id', '=', $skuAttrIdM1);
})
->join($m1ValueTable . ' as vt', function ($join) use ($attrM1) {
$join->on('e.entity_id', '=', 'vt.entity_id')
->where('vt.attribute_id', '=', $attrM1->attribute_id);
})
->where(function ($q) {
$q->whereNotNull('sku_v.value')
->orWhereNotNull('e.sku');
})
->selectRaw('e.entity_id as m1_entity_id, TRIM(COALESCE(sku_v.value, e.sku)) as sku, vt.store_id, vt.value')
->get();
if ($m1ProductsWithTabs->isEmpty()) {
return response()->json([
'success' => false,
'message' => "Attribute '{$attributeCode}' has {$m1ValueRowCount} value row(s) in M1, but no rows matched when joining with products (by SKU). Possible causes: SKU is stored in a different store_id in M1, or the value table does not match the attribute's backend_type.",
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
'diagnostic' => [
'backend_type' => $resolvedBackendType,
'm1_value_table' => $m1ValueTable,
'm1_value_row_count' => $m1ValueRowCount,
],
], 400);
}
$m2SkuToEntityId = DB::connection($this->magento2Connection)
->table($this->magento2Prefix . 'catalog_product_entity as e')
->leftJoin($this->magento2Prefix . 'catalog_product_entity_varchar as sku_v', function ($join) use ($skuAttrIdM2) {
$join->on('e.entity_id', '=', 'sku_v.entity_id')
->where('sku_v.attribute_id', '=', $skuAttrIdM2);
})
->where(function ($q) {
$q->whereNotNull('sku_v.value')
->orWhereNotNull('e.sku');
})
->selectRaw('e.entity_id, TRIM(COALESCE(sku_v.value, e.sku)) as sku')
->pluck('e.entity_id', 'sku');
$log = [];
$updated = 0;
$skipped = 0;
$skippedNoM2 = 0;
$skippedAlreadyMatch = 0;
$errors = 0;
foreach ($m1ProductsWithTabs as $row) {
$sku = trim((string) $row->sku);
$m2EntityId = $m2SkuToEntityId->get($sku);
if ($m2EntityId === null) {
$skipped++;
$skippedNoM2++;
$log[] = "SKU {$sku}: no M2 product, skipped.";
continue;
}
$existing = DB::connection($this->magento2Connection)
->table($m2ValueTable)
->where('entity_id', $m2EntityId)
->where('attribute_id', $attrM2->attribute_id)
->where('store_id', $row->store_id)
->first();
if ($existing && (string) $existing->value === (string) $row->value) {
$skipped++;
$skippedAlreadyMatch++;
$log[] = "SKU {$sku} (store {$row->store_id}): M2 value already matches M1, skipped.";
continue;
}
if ($dryRun) {
$updated++;
$log[] = "SKU {$sku} (store {$row->store_id}): would set value.";
continue;
}
try {
if ($existing) {
DB::connection($this->magento2Connection)
->table($m2ValueTable)
->where('entity_id', $m2EntityId)
->where('attribute_id', $attrM2->attribute_id)
->where('store_id', $row->store_id)
->update(['value' => $row->value]);
} else {
DB::connection($this->magento2Connection)
->table($m2ValueTable)
->insert([
'attribute_id' => $attrM2->attribute_id,
'store_id' => $row->store_id,
'entity_id' => $m2EntityId,
'value' => $row->value,
]);
}
$updated++;
$log[] = "SKU {$sku} (store {$row->store_id}): synced.";
} catch (\Throwable $e) {
$errors++;
$log[] = "SKU {$sku}: error - " . $e->getMessage();
Log::error("Sync frontpage tabs SKU {$sku}: " . $e->getMessage());
}
}
$m1SampleSkus = $m1ProductsWithTabs->take(5)->pluck('sku')->unique()->values()->toArray();
$diagnostic = [
'backend_type' => $resolvedBackendType,
'm1_value_row_count' => $m1ValueRowCount,
'm1_products_with_value' => $m1ProductsWithTabs->count(),
'skipped_no_m2_product' => $skippedNoM2,
'skipped_already_match' => $skippedAlreadyMatch,
'm1_sample_skus' => $m1SampleSkus,
];
return response()->json([
'success' => true,
'message' => $dryRun
? "Dry run: {$updated} product/store rows would be updated, {$skipped} skipped."
: "Synced frontpage tabs: {$updated} updated, {$skipped} skipped, {$errors} errors.",
'updated' => $updated,
'skipped' => $skipped,
'errors' => $errors,
'log' => array_slice($log, -100),
'diagnostic' => $diagnostic,
]);
} catch (\Throwable $e) {
Log::error('Sync frontpage tabs error: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Sync failed: ' . $e->getMessage(),
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'log' => [],
], 500);
}
}
}