option('connection'); $batchSize = max(1, (int) $this->option('batch-size')); $statusId = $this->option('status') !== null ? (int) $this->option('status') : null; $resetProgress = (bool) $this->option('reset-progress'); $dryRun = (bool) $this->option('dry-run'); $sync = (bool) $this->option('sync'); // ── Pre-flight: make sure the connection and source tables exist ───────── try { DB::connection($connection)->getPdo(); } catch (\Throwable $e) { $this->error("Cannot connect to Asteria DB (connection='{$connection}'): ".$e->getMessage()); $this->warn('Add an "asteria" entry to config/database.php (see the comment at the end of that file).'); return self::FAILURE; } if (! $this->ensureMigrationColumnExists()) { return self::FAILURE; } // ── Build product and customer lookup maps ──────────────────────────────── $this->info('Building product SKU map…'); $productIdMap = $this->buildProductIdMap($connection); $this->line(' '.count($productIdMap).' Magento products matched to Bagisto products by SKU.'); if (empty($productIdMap)) { $this->warn('No matching products found. Are the SKUs consistent between the two stores?'); } $this->info('Building customer e-mail map…'); $customerEmailMap = $this->buildCustomerEmailMap($connection); $this->line(' '.count($customerEmailMap).' Magento customers matched to Bagisto customers by e-mail.'); // ── Determine starting review_id ────────────────────────────────────────── $lastId = $resetProgress ? 0 : (int) Cache::get(self::PROGRESS_KEY, 0); if ($lastId > 0) { $this->line("Resuming from Asteria review_id > {$lastId} (use --reset-progress to restart)."); } // ── Dispatch batches ────────────────────────────────────────────────────── $totalDispatched = 0; $totalSkipped = 0; $batchNumber = 0; $this->info($dryRun ? '[DRY RUN] Scanning reviews…' : 'Dispatching migration jobs…'); do { $rows = $this->fetchBatch($connection, $lastId, $batchSize, $statusId); if ($rows->isEmpty()) { break; } $batchNumber++; $batchRows = $rows->toArray(); // Collect unique Magento product IDs in this batch, filter unknown ones. $unknownProducts = array_filter( array_unique(array_column($batchRows, 'magento_product_id')), fn ($id) => ! isset($productIdMap[$id]) ); if (! empty($unknownProducts)) { $this->warn(" Batch #{$batchNumber}: ".count($unknownProducts). ' Magento product(s) not found in Bagisto: '.implode(', ', array_slice($unknownProducts, 0, 10))); $totalSkipped += count(array_filter($batchRows, fn ($r) => ! isset($productIdMap[$r->magento_product_id]))); } $lastId = max(array_column($batchRows, 'review_id')); if (! $dryRun) { if ($sync) { (new MigrateReviewJob( array_map(fn ($r) => (array) $r, $batchRows), $productIdMap, $customerEmailMap, ))->handle(); } else { MigrateReviewJob::dispatch( array_map(fn ($r) => (array) $r, $batchRows), $productIdMap, $customerEmailMap, )->onQueue(self::QUEUE_NAME); } Cache::put(self::PROGRESS_KEY, $lastId, now()->addDays(30)); } $totalDispatched += count($batchRows); $this->line(sprintf( ' Batch #%d: %d reviews (last review_id=%d)%s', $batchNumber, count($batchRows), $lastId, $dryRun ? ' [skipped – dry-run]' : ($sync ? ' [processed]' : ' [enqueued]'), )); Log::info('MigrateAsteriaReviews: batch '.$batchNumber.', '.count($batchRows).' rows, last_id='.$lastId); } while ($rows->count() === $batchSize); // ── Summary ─────────────────────────────────────────────────────────────── $this->newLine(); $this->info("Done. Batches: {$batchNumber}, reviews processed/enqueued: {$totalDispatched}, unmatched: {$totalSkipped}."); if (! $dryRun && ! $sync) { $this->line('Run php artisan queue:work --queue=review-migration to process the queue.'); } return self::SUCCESS; } // ───────────────────────────────────────────────────────────────────────────── // Private helpers // ───────────────────────────────────────────────────────────────────────────── /** * Fetch one page of Magento reviews, enriched with rating and customer e-mail. * * Magento table names use the default prefix (none). If your Magento * installation uses a prefix (e.g. `mag_`), set ASTERIA_DB_PREFIX in .env and * pass it via the `prefix` key in the database connection config. * * @return \Illuminate\Support\Collection */ private function fetchBatch( string $connection, int $lastId, int $limit, ?int $statusId ): \Illuminate\Support\Collection { $query = DB::connection($connection) ->table('review AS r') ->join('review_detail AS rd', 'rd.review_id', '=', 'r.review_id') ->leftJoin('customer_entity AS ce', 'ce.entity_id', '=', 'rd.customer_id') ->leftJoinSub( // Average all rating dimensions for each review (Quality, Value, Price…) DB::connection($connection) ->table('rating_option_vote') ->select('review_id', DB::raw('AVG(`value`) AS avg_rating')) ->groupBy('review_id'), 'rv', 'rv.review_id', '=', 'r.review_id' ) ->select([ 'r.review_id', 'r.created_at', 'r.entity_pk_value AS magento_product_id', 'r.status_id', 'rd.title', 'rd.detail AS comment', 'rd.nickname AS name', 'rd.customer_id AS magento_customer_id', 'ce.email AS customer_email', 'rv.avg_rating', ]) ->where('r.entity_id', 1) // entity_id=1 means "product" in Magento ->where('r.review_id', '>', $lastId) ->orderBy('r.review_id') ->limit($limit); if ($statusId !== null) { $query->where('r.status_id', $statusId); } $reviews = $query->get(); if ($reviews->isEmpty()) { return $reviews; } // Attach image URLs from Senje_Review's review_media_image table (status_id=1 = approved). $reviewIds = $reviews->pluck('review_id')->toArray(); $imageRows = DB::connection($connection) ->table('review_media_image') ->select('review_id', 'image_id', 'url') ->whereIn('review_id', $reviewIds) ->where('status_id', 1) ->orderBy('review_id') ->orderBy('image_id') ->get() ->groupBy('review_id'); return $reviews->map(function ($row) use ($imageRows) { $row->images = $imageRows->get($row->review_id, collect()) ->pluck('url') ->toArray(); return $row; }); } /** * Build a map of magento_product_id → bagisto_product_id by joining * Magento's catalog_product_entity.sku against Bagisto's products.sku. * * @return array */ private function buildProductIdMap(string $connection): array { // Fetch all Magento SKUs. $magentoPairs = DB::connection($connection) ->table('catalog_product_entity') ->select('entity_id', 'sku') ->get() ->pluck('entity_id', 'sku') // sku → magento_id ->toArray(); if (empty($magentoPairs)) { return []; } // Match against Bagisto's products table. $skus = array_keys($magentoPairs); $map = []; foreach (array_chunk($skus, 500) as $chunk) { $rows = DB::table('products') ->select('id', 'sku') ->whereIn('sku', $chunk) ->get(); foreach ($rows as $row) { $magentoId = $magentoPairs[$row->sku] ?? null; if ($magentoId !== null) { $map[(int) $magentoId] = (int) $row->id; } } } return $map; } /** * Build a map of lowercase_email → bagisto_customer_id. * * @return array */ private function buildCustomerEmailMap(string $connection): array { // Fetch Magento customer e-mails. $magentoEmails = DB::connection($connection) ->table('customer_entity') ->select('email') ->get() ->pluck('email') ->map(fn ($e) => strtolower((string) $e)) ->unique() ->toArray(); if (empty($magentoEmails)) { return []; } $map = []; foreach (array_chunk($magentoEmails, 500) as $chunk) { $rows = DB::table('customers') ->select('id', 'email') ->whereIn(DB::raw('LOWER(email)'), $chunk) ->get(); foreach ($rows as $row) { $map[strtolower($row->email)] = (int) $row->id; } } return $map; } /** * Ensure product_reviews has the deduplication column. * Prints instructions and returns false if the column is absent and * cannot be auto-created (requires ALTER TABLE permission). */ private function ensureMigrationColumnExists(): bool { $hasColumn = Schema::hasColumn('product_reviews', 'migrated_from_asteria_id'); if ($hasColumn) { return true; } $this->warn('Column product_reviews.migrated_from_asteria_id is missing.'); $this->line('Run the following migration or SQL:'); $this->line(''); $this->line(' php artisan make:migration add_migrated_from_asteria_id_to_product_reviews'); $this->line(' Then add to the up() method:'); $this->line(' $table->bigInteger(\'migrated_from_asteria_id\')->unsigned()->nullable()->unique();'); $this->line(''); $this->line(' Or directly in MySQL:'); $this->line(' ALTER TABLE product_reviews'); $this->line(' ADD COLUMN migrated_from_asteria_id BIGINT UNSIGNED NULL DEFAULT NULL,'); $this->line(' ADD UNIQUE INDEX uq_migrated_asteria_id (migrated_from_asteria_id);'); $this->line(''); if (! $this->confirm('Attempt to add the column automatically now?', false)) { return false; } try { DB::statement(' ALTER TABLE product_reviews ADD COLUMN migrated_from_asteria_id BIGINT UNSIGNED NULL DEFAULT NULL, ADD UNIQUE INDEX uq_migrated_asteria_id (migrated_from_asteria_id) '); $this->info('Column added successfully.'); return true; } catch (\Throwable $e) { $this->error('Could not add column automatically: '.$e->getMessage()); return false; } } }