*/ private const CATALOG_TABLES = [ // Longyi flexible variants 'product_variant_option_values', 'product_variant_images', 'product_variants', 'product_product_options', 'product_option_values', 'product_options', // Q&A / reviews 'product_question_answer_votes', 'product_question_answers', 'product_questions', 'product_review_images', 'product_reviews', // Booking 'booking_product_event_ticket_translations', 'booking_product_event_tickets', 'booking_product_appointment_slots', 'booking_product_default_slots', 'booking_product_rental_slots', 'booking_product_table_slots', 'booking_products', // Bundle / grouped / customizable / downloadable 'product_bundle_option_products', 'product_bundle_option_translations', 'product_bundle_options', 'product_grouped_products', 'product_customizable_option_prices', 'product_customizable_option_translations', 'product_customizable_options', 'product_downloadable_link_translations', 'product_downloadable_links', 'product_downloadable_sample_translations', 'product_downloadable_samples', // Catalog rule product indices (not the rules themselves) 'catalog_rule_product_prices', 'catalog_rule_products', // Runtime references 'cart_item_inventories', 'cart_items', 'wishlist_items', 'wishlist', 'compare_items', // Associations / media / inventory / indices 'product_cross_sells', 'product_up_sells', 'product_relations', 'product_super_attributes', 'product_categories', 'product_channels', 'product_attribute_values', 'product_images', 'product_videos', 'product_inventories', 'product_ordered_inventories', 'product_inventory_indices', 'product_salable_inventories', 'product_price_indices', 'product_customer_group_prices', 'product_flat', 'products', ]; /** * Historical rows that keep their snapshot but must not block deleting products. * * @var list */ private const DETACH_PRODUCT_ID = [ ['order_items', 'product_id'], ['invoice_items', 'product_id'], ['shipment_items', 'product_id'], ['refund_items', 'product_id'], ['bookings', 'product_id'], ]; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $tables = $this->existingTables(self::CATALOG_TABLES); $this->warn('This clears catalog products so they can be re-imported.'); $this->line('Kept: attributes, categories, customers, orders (SKU snapshot stays; product_id set null).'); $this->line('Removed: products, variants, inventories, reviews, Q&A, cart items, wishlists, compare items.'); $this->newLine(); $rows = $this->countRows($tables); $this->table(['table', 'rows'], collect($rows)->map(fn ($count, $table) => [$table, $count])->values()->all()); $total = array_sum($rows); $this->info("Catalog rows that would be truncated: {$total}"); $detachCounts = $this->countDetachRows(); if ($detachCounts !== []) { $this->newLine(); $this->comment('Historical rows whose product_id will be set to NULL:'); $this->table(['table', 'rows with product_id'], collect($detachCounts)->map(fn ($count, $table) => [$table, $count])->values()->all()); } $rewriteCount = $this->countProductUrlRewrites(); if ($rewriteCount > 0) { $this->comment("url_rewrites (entity_type=product): {$rewriteCount}"); } if ($dryRun) { $this->info('Dry run — nothing written.'); return self::SUCCESS; } if (! $this->option('force') && ! $this->confirm('Truncate catalog tables now?', false)) { $this->info('Aborted.'); return self::SUCCESS; } $driver = DB::getDriverName(); try { $this->disableForeignKeyChecks($driver); $this->detachHistoricalProductIds(); $this->deleteProductUrlRewrites(); $this->deleteProductVisits(); foreach ($tables as $table) { $this->emptyTable($driver, $table); } } finally { $this->enableForeignKeyChecks($driver); } if ($this->option('purge-files')) { Storage::deleteDirectory('product'); $this->info('Deleted storage directory: product/'); } $this->wipeElasticsearchIndices(); $this->newLine(); $this->info('Catalog reset finished. Next:'); $this->line(' php artisan products:migrate-asteria'); $this->line(' php artisan reviews:migrate-asteria --reset-progress --sync # if reviews should be re-imported'); return self::SUCCESS; } /** * @param list $tables * @return list */ private function existingTables(array $tables): array { return array_values(array_filter($tables, fn (string $table) => Schema::hasTable($table))); } /** * @param list $tables * @return array */ private function countRows(array $tables): array { $counts = []; foreach ($tables as $table) { $counts[$table] = (int) DB::table($table)->count(); } return $counts; } /** * @return array */ private function countDetachRows(): array { $counts = []; foreach (self::DETACH_PRODUCT_ID as [$table, $column]) { if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { continue; } $count = (int) DB::table($table)->whereNotNull($column)->count(); if ($count > 0) { $counts[$table] = $count; } } return $counts; } private function countProductUrlRewrites(): int { if (! Schema::hasTable('url_rewrites')) { return 0; } return (int) DB::table('url_rewrites')->where('entity_type', 'product')->count(); } private function detachHistoricalProductIds(): void { foreach (self::DETACH_PRODUCT_ID as [$table, $column]) { if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { continue; } DB::table($table)->whereNotNull($column)->update([$column => null]); } } private function deleteProductUrlRewrites(): void { if (! Schema::hasTable('url_rewrites')) { return; } DB::table('url_rewrites')->where('entity_type', 'product')->delete(); } private function deleteProductVisits(): void { if (! Schema::hasTable('visits') || ! Schema::hasColumn('visits', 'visitable_type')) { return; } DB::table('visits')->where('visitable_type', 'like', '%Product%')->delete(); } private function emptyTable(string $driver, string $table): void { if ($driver === 'mysql') { DB::statement('TRUNCATE TABLE '.$this->quoteTable($table)); return; } DB::table($table)->delete(); if ($driver === 'sqlite') { DB::table('sqlite_sequence')->where('name', DB::getTablePrefix().$table)->delete(); } } private function quoteTable(string $table): string { $name = DB::getTablePrefix().$table; return '`'.str_replace('`', '``', $name).'`'; } private function disableForeignKeyChecks(string $driver): void { if ($driver === 'mysql') { DB::statement('SET FOREIGN_KEY_CHECKS=0'); } elseif ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = OFF'); } } private function enableForeignKeyChecks(string $driver): void { if ($driver === 'mysql') { DB::statement('SET FOREIGN_KEY_CHECKS=1'); } elseif ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = ON'); } } private function wipeElasticsearchIndices(): void { if (core()->getConfigData('catalog.products.search.engine') !== 'elastic') { return; } try { $channels = Channel::with('locales')->get(); foreach ($channels as $channel) { foreach ($channel->locales as $locale) { $index = ProductIndexName::formatElasticSearchIndexName($channel->code, $locale->code); try { ElasticSearch::indices()->delete(['index' => $index]); $this->comment("Deleted Elasticsearch index: {$index}"); } catch (Throwable $e) { $this->comment("Elasticsearch index {$index} skipped: ".$e->getMessage()); } } } } catch (Throwable $e) { $this->warn('Elasticsearch wipe skipped: '.$e->getMessage()); } } }