magento1Prefix = config('database.connections.magento1.prefix', ''); $this->magento2Prefix = config('database.connections.magento2.prefix', ''); } /** * Show the product URLs comparison page */ public function index() { return view('product-urls.index'); } /** * Get product URLs from Magento 1 */ public function getMagento1Urls(Request $request) { try { $sku = $request->input('sku'); $limit = $request->input('limit', 100); $offset = $request->input('offset', 0); $query = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'core_url_rewrite as ur') ->join($this->magento1Prefix . 'catalog_product_entity as p', function($join) { // Match exactly `product/{id}` or `product/{id}/...` so we don't // false-match product 1 against product 10/100/1000/etc. $join->whereRaw("(ur.id_path = CONCAT('product/', p.entity_id) OR ur.id_path LIKE CONCAT('product/', p.entity_id, '/%'))"); }) ->select( 'p.entity_id', 'p.sku', 'ur.request_path', 'ur.target_path', 'ur.store_id', 'ur.id_path' ) ->where('ur.id_path', 'like', 'product/%'); if ($sku) { $query->where('p.sku', 'like', '%' . $sku . '%'); } $total = $query->count(); $urls = $query->orderBy('p.entity_id') ->orderBy('ur.store_id') ->limit($limit) ->offset($offset) ->get(); return response()->json([ 'success' => true, 'urls' => $urls, 'total' => $total, 'limit' => $limit, 'offset' => $offset ]); } catch (\Exception $e) { Log::error('Error fetching Magento 1 URLs: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to fetch Magento 1 URLs: ' . $e->getMessage(), 'urls' => [], 'total' => 0 ], 500); } } /** * Get product URLs from Magento 2 */ public function getMagento2Urls(Request $request) { try { $sku = $request->input('sku'); $limit = $request->input('limit', 100); $offset = $request->input('offset', 0); $query = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'url_rewrite as ur') ->join($this->magento2Prefix . 'catalog_product_entity as p', function($join) { $join->whereRaw("ur.entity_id = p.entity_id AND ur.entity_type = 'product'"); }) ->select( 'p.entity_id', 'p.sku', 'ur.request_path', 'ur.target_path', 'ur.store_id', 'ur.entity_id as url_entity_id' ) ->where('ur.entity_type', 'product'); if ($sku) { $query->where('p.sku', 'like', '%' . $sku . '%'); } $total = $query->count(); $urls = $query->orderBy('p.entity_id') ->orderBy('ur.store_id') ->limit($limit) ->offset($offset) ->get(); return response()->json([ 'success' => true, 'urls' => $urls, 'total' => $total, 'limit' => $limit, 'offset' => $offset ]); } catch (\Exception $e) { Log::error('Error fetching Magento 2 URLs: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to fetch Magento 2 URLs: ' . $e->getMessage(), 'urls' => [], 'total' => 0 ], 500); } } /** * Compare product URLs between Magento 1 and Magento 2. * * A product can have multiple rewrites per store (canonical + one per category), * so the comparison key is (sku, store_id, request_path). We restrict to catalog * product URLs (target_path LIKE 'catalog/product/view/%') so this aligns with * what the Fix tool migrates - non-catalog rewrites such as the productquestions * module are deliberately excluded from migration and from comparison. */ public function compareUrls(Request $request) { try { $sku = $request->input('sku'); $limit = (int) $request->input('limit', 100); $offset = (int) $request->input('offset', 0); $m1Query = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'core_url_rewrite as ur') ->join($this->magento1Prefix . 'catalog_product_entity as p', function ($join) { $join->whereRaw("(ur.id_path = CONCAT('product/', p.entity_id) OR ur.id_path LIKE CONCAT('product/', p.entity_id, '/%'))"); }) ->select( 'p.entity_id as product_id', 'p.sku', 'ur.request_path', 'ur.store_id' ) ->where('ur.target_path', 'like', 'catalog/product/view/%'); if ($sku) { $m1Query->where('p.sku', 'like', '%' . $sku . '%'); } $m1Rows = $m1Query->get(); $m2Query = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'url_rewrite as ur') ->join($this->magento2Prefix . 'catalog_product_entity as p', function ($join) { $join->whereRaw("ur.entity_id = p.entity_id AND ur.entity_type = 'product'"); }) ->select( 'p.entity_id as product_id', 'p.sku', 'ur.request_path', 'ur.store_id' ) ->where('ur.entity_type', 'product') ->where('ur.target_path', 'like', 'catalog/product/view/%'); if ($sku) { $m2Query->where('p.sku', 'like', '%' . $sku . '%'); } $m2Rows = $m2Query->get(); $key = fn($r) => $r->sku . '|' . $r->store_id . '|' . $r->request_path; $m1Map = []; foreach ($m1Rows as $r) { $m1Map[$key($r)] = $r; } $m2Map = []; foreach ($m2Rows as $r) { $m2Map[$key($r)] = $r; } // sku -> product_id on each side (any row will do; same product per sku). $m1Pid = []; foreach ($m1Rows as $r) { $m1Pid[$r->sku] = $r->product_id; } $m2Pid = []; foreach ($m2Rows as $r) { $m2Pid[$r->sku] = $r->product_id; } $allKeys = array_keys($m1Map + $m2Map); sort($allKeys); $comparison = []; foreach ($allKeys as $k) { $m1r = $m1Map[$k] ?? null; $m2r = $m2Map[$k] ?? null; $row = $m1r ?? $m2r; if ($m1r && $m2r) { $status = 'match'; } elseif ($m1r) { $status = 'missing_in_m2'; } else { $status = 'missing_in_m1'; } $comparison[] = [ 'sku' => $row->sku, 'm1_product_id' => $m1Pid[$row->sku] ?? null, 'm2_product_id' => $m2Pid[$row->sku] ?? null, 'store_id' => $row->store_id, 'm1_url' => $m1r->request_path ?? null, 'm2_url' => $m2r->request_path ?? null, 'status' => $status, ]; } $total = count($comparison); $paginated = array_slice($comparison, $offset, $limit); return response()->json([ 'success' => true, 'comparison' => $paginated, 'total' => $total, 'limit' => $limit, 'offset' => $offset, 'summary' => [ 'match' => count(array_filter($comparison, fn($c) => $c['status'] === 'match')), 'missing_in_m2' => count(array_filter($comparison, fn($c) => $c['status'] === 'missing_in_m2')), 'missing_in_m1' => count(array_filter($comparison, fn($c) => $c['status'] === 'missing_in_m1')), 'different' => 0, ], ]); } catch (\Exception $e) { Log::error('Error comparing URLs: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to compare URLs: ' . $e->getMessage(), 'comparison' => [], 'total' => 0 ], 500); } } /** * Migrate / fix product URL rewrites from M1 -> M2. * * Reads M1 core_url_rewrite rows whose target_path is `catalog/product/view/...` * (the canonical and category-bound product URLs) and inserts the corresponding * row into M2 url_rewrite. Non-catalog rewrites such as the productquestions * module are skipped, the M2 product is looked up by SKU so target_path uses * the M2 entity_id, and category-bound URLs get metadata={"category_id":"X"}. * * Params: * sku (optional) limit to a single product * dry_run (optional) preview without inserting */ public function fixProductUrls(Request $request) { try { $sku = $request->input('sku'); $dryRun = filter_var($request->input('dry_run', false), FILTER_VALIDATE_BOOLEAN); $storeMapping = $this->getStoreMapping(); $query = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'core_url_rewrite as ur') ->join($this->magento1Prefix . 'catalog_product_entity as p', function ($join) { $join->whereRaw("(ur.id_path = CONCAT('product/', p.entity_id) OR ur.id_path LIKE CONCAT('product/', p.entity_id, '/%'))"); }) ->select( 'p.entity_id as m1_product_id', 'p.sku', 'ur.url_rewrite_id', 'ur.store_id', 'ur.id_path', 'ur.request_path', 'ur.target_path', 'ur.is_system', 'ur.description' ) ->where('ur.target_path', 'like', 'catalog/product/view/%'); if (!empty($sku)) { $query->where('p.sku', $sku); } $m1Rewrites = $query->orderBy('p.entity_id')->orderBy('ur.store_id')->get(); $added = 0; $skippedExisting = 0; $skippedNoProduct = 0; $errors = 0; $log = []; $m2ProductCache = []; foreach ($m1Rewrites as $r) { try { $m1StoreId = (int) ($r->store_id ?? 0); $m2StoreId = $storeMapping[$m1StoreId] ?? $m1StoreId; if (!isset($m2ProductCache[$r->sku])) { $m2ProductCache[$r->sku] = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_entity') ->where('sku', $r->sku) ->value('entity_id'); } $m2ProductId = $m2ProductCache[$r->sku]; if (!$m2ProductId) { $skippedNoProduct++; $log[] = "SKIP no M2 product for sku '{$r->sku}' (M1 id={$r->m1_product_id}, request_path={$r->request_path})"; continue; } // M1 id_path is either `product/{id}` (canonical) or // `product/{id}/{category_id}` (category-bound). $idPathParts = explode('/', $r->id_path); $categoryId = $idPathParts[2] ?? null; $targetPath = 'catalog/product/view/id/' . $m2ProductId; $metadata = null; if (!empty($categoryId) && ctype_digit((string) $categoryId)) { $targetPath .= '/category/' . $categoryId; $metadata = json_encode(['category_id' => (string) $categoryId]); } // M2 unique key is (request_path, store_id), so check exactly that. $exists = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'url_rewrite') ->where('request_path', $r->request_path) ->where('store_id', $m2StoreId) ->exists(); if ($exists) { $skippedExisting++; continue; } if ($dryRun) { $added++; $log[] = "WOULD ADD [{$r->sku}] store={$m2StoreId} {$r->request_path} -> {$targetPath}"; continue; } DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'url_rewrite') ->insert([ 'entity_type' => 'product', 'entity_id' => $m2ProductId, 'request_path' => $r->request_path, 'target_path' => $targetPath, 'redirect_type' => 0, 'store_id' => $m2StoreId, 'description' => $r->description, 'is_autogenerated' => ($r->is_system ?? 0) ? 1 : 0, 'metadata' => $metadata, ]); $added++; } catch (\Exception $e) { $errors++; $log[] = "ERROR [{$r->sku}] {$r->request_path}: " . $e->getMessage(); Log::error("fixProductUrls error for sku '{$r->sku}', request_path '{$r->request_path}': " . $e->getMessage()); } } return response()->json([ 'success' => true, 'dry_run' => $dryRun, 'sku' => $sku, 'added' => $added, 'skipped_existing' => $skippedExisting, 'skipped_no_m2_product'=> $skippedNoProduct, 'errors' => $errors, 'total_m1_rewrites' => $m1Rewrites->count(), 'log' => array_slice($log, 0, 500), ]); } catch (\Exception $e) { Log::error('fixProductUrls fatal: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Fix failed: ' . $e->getMessage(), ], 500); } } /** * Map M1 store_id -> M2 store_id by store code (case-insensitive, trimmed, * tolerant of M2's "_N" suffix that DDEV/Magento sometimes appends), with * fall-through to identity mapping when codes don't line up. */ protected function getStoreMapping() { $m1Stores = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'core_store') ->where('store_id', '>', 0) ->get(); $m2Stores = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'store') ->where('store_id', '>', 0) ->get(); $m2ByCode = []; $m2ById = []; foreach ($m2Stores as $s) { $m2ById[$s->store_id] = $s; $code = strtolower(trim($s->code ?? '')); if ($code !== '') { $m2ByCode[$code] = $s->store_id; // M2 sometimes carries a `_N` suffix on the code; index the bare prefix too. $bare = preg_replace('/_\d+$/', '', $code); if ($bare !== $code && !isset($m2ByCode[$bare])) { $m2ByCode[$bare] = $s->store_id; } } } $mapping = []; foreach ($m1Stores as $m1) { $code = strtolower(trim($m1->code ?? '')); if ($code !== '' && isset($m2ByCode[$code])) { $mapping[$m1->store_id] = $m2ByCode[$code]; } elseif (isset($m2ById[$m1->store_id])) { $mapping[$m1->store_id] = $m1->store_id; } } return $mapping; } /** * Compare URLs for a single product SKU */ public function compareSingleSku(Request $request) { try { $sku = $request->input('sku'); if (empty($sku)) { return response()->json([ 'success' => false, 'message' => 'SKU is required', 'comparison' => [] ], 400); } // Get M1 catalog product URLs for this SKU $m1Urls = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'core_url_rewrite as ur') ->join($this->magento1Prefix . 'catalog_product_entity as p', function($join) { $join->whereRaw("(ur.id_path = CONCAT('product/', p.entity_id) OR ur.id_path LIKE CONCAT('product/', p.entity_id, '/%'))"); }) ->select( 'p.entity_id as product_id', 'p.sku', 'ur.request_path as m1_url', 'ur.target_path as m1_target_path', 'ur.store_id as m1_store_id', 'ur.id_path' ) ->where('ur.target_path', 'like', 'catalog/product/view/%') ->where('p.sku', $sku) ->get() ->groupBy('m1_store_id'); // Get M2 catalog product URLs for this SKU $m2Urls = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'url_rewrite as ur') ->join($this->magento2Prefix . 'catalog_product_entity as p', function($join) { $join->whereRaw("ur.entity_id = p.entity_id AND ur.entity_type = 'product'"); }) ->select( 'p.entity_id as product_id', 'p.sku', 'ur.request_path as m2_url', 'ur.target_path as m2_target_path', 'ur.store_id as m2_store_id' ) ->where('ur.entity_type', 'product') ->where('ur.target_path', 'like', 'catalog/product/view/%') ->where('p.sku', $sku) ->get() ->groupBy('m2_store_id'); // Get product info $m1Product = DB::connection($this->magento1Connection) ->table($this->magento1Prefix . 'catalog_product_entity') ->where('sku', $sku) ->first(); $m2Product = DB::connection($this->magento2Connection) ->table($this->magento2Prefix . 'catalog_product_entity') ->where('sku', $sku) ->first(); // Compare URLs by store - return ALL URLs for each store $comparison = []; $allStores = array_unique(array_merge( $m1Urls->keys()->toArray(), $m2Urls->keys()->toArray() )); foreach ($allStores as $storeId) { $m1StoreUrls = $m1Urls->get($storeId, collect([])); $m2StoreUrls = $m2Urls->get($storeId, collect([])); // Get all M1 URLs for this store $m1UrlsList = $m1StoreUrls->map(function($item) { return [ 'url' => $item->m1_url ?? null, 'target_path' => $item->m1_target_path ?? null, 'id_path' => $item->id_path ?? null ]; })->toArray(); // Get all M2 URLs for this store $m2UrlsList = $m2StoreUrls->map(function($item) { return [ 'url' => $item->m2_url ?? null, 'target_path' => $item->m2_target_path ?? null ]; })->toArray(); // Per-store status uses set semantics (URL order is not stable, so a // positional compare would falsely flag rearranged but identical sets // as 'different'). 'different' here means both sides have URLs but // the sets aren't equal - i.e. some are missing on one side. $m1UrlStrings = array_filter(array_column($m1UrlsList, 'url')); $m2UrlStrings = array_filter(array_column($m2UrlsList, 'url')); $m1Set = array_unique($m1UrlStrings); $m2Set = array_unique($m2UrlStrings); if (empty($m1Set) && empty($m2Set)) { $status = 'match'; } elseif (!empty($m1Set) && empty($m2Set)) { $status = 'missing_in_m2'; } elseif (empty($m1Set) && !empty($m2Set)) { $status = 'missing_in_m1'; } else { sort($m1Set); sort($m2Set); $status = ($m1Set === $m2Set) ? 'match' : 'different'; } $comparison[] = [ 'store_id' => $storeId, 'm1_urls' => $m1UrlsList, 'm2_urls' => $m2UrlsList, 'status' => $status ]; } // Calculate summary statistics $summary = [ 'match' => 0, 'missing_in_m2' => 0, 'missing_in_m1' => 0, 'different' => 0, 'total_stores' => count($comparison), 'total_m1_urls' => 0, 'total_m2_urls' => 0 ]; foreach ($comparison as $store) { $summary['total_m1_urls'] += count($store['m1_urls']); $summary['total_m2_urls'] += count($store['m2_urls']); switch ($store['status']) { case 'match': $summary['match']++; break; case 'missing_in_m2': $summary['missing_in_m2']++; break; case 'missing_in_m1': $summary['missing_in_m1']++; break; case 'different': $summary['different']++; break; } } return response()->json([ 'success' => true, 'sku' => $sku, 'm1_product_id' => $m1Product->entity_id ?? null, 'm2_product_id' => $m2Product->entity_id ?? null, 'comparison' => $comparison, 'summary' => $summary ]); } catch (\Exception $e) { Log::error('Error comparing single SKU URLs: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to compare URLs: ' . $e->getMessage(), 'comparison' => [] ], 500); } } }