Automattic\WooCommerce\Internal\CLI\Migrator\Core
WooCommerceProductImporter{} │ WC 1.0
WooCommerceProductImporter class.
Handles the creation and updating of WooCommerce products from mapped data. This class focuses on the actual product creation logic, following WordPress coding standards and our established architecture patterns.
Hooks from the class
Usage
$WooCommerceProductImporter = new WooCommerceProductImporter(); // use class methods
Methods
- public __construct()
- public add_avif_support_to_sideload( array $allowed_extensions )
- public configure( array $options )
- public get_import_stats()
- public import_batch( array $products_data, array $source_data_batch = array() )
- public import_product( array $product_data, array $source_data = array() )
- public optimize_http_request_args( array $args )
- public reset_stats()
- public set_image_download_timeout()
- public set_progress_callback( ?callable $callback )
- private create_error_result( string $error_code, string $message, array $product_data )
- private create_product_object( string $product_type )
- private create_product_variations( int $parent_id, array $variations )
- private create_success_result( string $action, int $product_id, string $message )
- private determine_product_type( array $product_data )
- private find_existing_product( array $product_data )
- private get_attachment_by_url( string $image_url )
- private get_default_options()
- private get_or_create_product_object( ?int $existing_product_id, string $required_type )
- private get_or_create_terms( array $terms_data, string $taxonomy )
- private handle_post_save_operations( int $product_id, array $product_data )
- private handle_product_images( WC_Product $product, array $images_data )
- private handle_simple_product( WC_Product_Simple $product, array $product_data )
- private handle_variable_product( WC_Product_Variable $product, array $product_data )
- private import_image( string $image_url, string $alt_text = '', int $product_id = 0 )
- private import_image_with_mapping( string $image_url, string $alt_text = '', ?string $original_id = null, int $product_id = 0 )
- private set_basic_product_properties( WC_Product $product, array $product_data )
- private set_cogs_value_direct( WC_Product $product, float $cogs_value )
- private set_product_attributes( WC_Product $product, array $attributes )
- private set_product_taxonomies( WC_Product $product, array $product_data )
- private setup_attributes( WC_Product_Variable $product, array $attributes_data )
- private sync_variations( WC_Product_Variable $product, array $variations_data )
- private update_seo_meta( int $product_id, array $metafields, array $product_data )
- private validate_product_data( array $product_data )
WooCommerceProductImporter{} WooCommerceProductImporter{} code WC 10.7.0
class WooCommerceProductImporter {
/**
* Default timeout for image downloads in seconds.
*
* @var int
*/
private const DEFAULT_IMAGE_TIMEOUT = 10;
/**
* Maximum number of images to process per product.
*
* @var int
*/
private const MAX_IMAGES_PER_PRODUCT = 50;
/**
* Import options and configuration.
*
* @var array
*/
private array $import_options;
/**
* Progress callback function for per-product updates.
*
* @var callable|null
*/
private $progress_callback = null;
/**
* Statistics tracking for import operations.
*
* @var array
*/
private array $import_stats = array(
'products_created' => 0,
'products_updated' => 0,
'products_skipped' => 0,
'images_processed' => 0,
'errors_encountered' => 0,
);
/**
* Migration data including image and variation mappings for session persistence.
*
* @var array
*/
private array $migration_data = array(
'images_mapping' => array(),
'variations_mapping' => array(),
);
/**
* Mapping of original attribute names to taxonomy names for current product.
*
* @var array
*/
private array $current_attribute_mapping = array();
/**
* Constructor - parameterless to support WooCommerce DI container.
*/
public function __construct() {
$this->import_options = $this->get_default_options();
}
/**
* Configure the importer with options.
*
* @param array $options Import options and configuration.
*/
public function configure( array $options ): void {
$this->import_options = array_merge( $this->import_options, $options );
}
/**
* Set progress callback for per-product import updates.
*
* @param callable|null $callback Function to call with progress updates.
* Receives: (current_index, total_count, product_name, result).
*/
public function set_progress_callback( ?callable $callback ): void {
$this->progress_callback = $callback;
}
/**
* Import a single product from mapped data.
*
* @param array $product_data Mapped WooCommerce product data.
* @param array $source_data Original source platform data for reference.
* @return array Import result with status and details.
*/
public function import_product( array $product_data, array $source_data = array() ): array {
$start_time = microtime( true );
$product_name = $product_data['name'] ?? 'Unknown Product';
$this->current_attribute_mapping = array();
try {
wc_get_logger()->info( "Starting import for product: {$product_name}", array( 'source' => 'wc-migrator' ) );
$validation_result = $this->validate_product_data( $product_data );
if ( ! $validation_result['valid'] ) {
wc_get_logger()->error( "Validation failed for product: {$product_name} - " . $validation_result['message'], array( 'source' => 'wc-migrator' ) );
return $this->create_error_result( 'validation_failed', $validation_result['message'], $product_data );
}
$existing_product_id = $this->find_existing_product( $product_data, $source_data );
if ( $existing_product_id && $this->import_options['skip_existing'] ) {
++$this->import_stats['products_skipped'];
return $this->create_success_result( 'skipped', $existing_product_id, 'Product already exists and skip_existing is enabled' );
}
$product_type = $this->determine_product_type( $product_data );
$product = $this->get_or_create_product_object( $existing_product_id, $product_type );
if ( ! $product ) {
return $this->create_error_result( 'product_creation_failed', 'Failed to create product object', $product_data );
}
if ( $existing_product_id ) {
$existing_migration_data = $product->get_meta( '_migration_data' );
if ( is_array( $existing_migration_data ) ) {
$this->migration_data['images_mapping'] = $existing_migration_data['images_mapping'] ?? array();
$this->migration_data['variations_mapping'] = $existing_migration_data['variations_mapping'] ?? array();
}
}
$this->set_basic_product_properties( $product, $product_data );
$this->set_product_taxonomies( $product, $product_data );
$this->handle_product_images( $product, $product_data['images'] ?? array() );
wc_get_logger()->debug( "Processing {$product_type} product: {$product_name}", array( 'source' => 'wc-migrator' ) );
switch ( $product_type ) {
case 'variable':
$this->handle_variable_product( $product, $product_data );
break;
case 'simple':
default:
$this->handle_simple_product( $product, $product_data );
break;
}
$product_id = $product->save();
if ( ! $product_id ) {
return $this->create_error_result( 'save_failed', 'Failed to save product to database', $product_data );
}
$this->handle_post_save_operations( $product_id, $product_data, $source_data );
if ( $existing_product_id ) {
++$this->import_stats['products_updated'];
} else {
++$this->import_stats['products_created'];
}
$duration = microtime( true ) - $start_time;
$action = $existing_product_id ? 'updated' : 'created';
wc_get_logger()->info(
"Successfully {$action} product: {$product_name} (ID: {$product_id}) in {$duration}s",
array( 'source' => 'wc-migrator' )
);
return $this->create_success_result( $action, $product_id, "Product {$action} successfully in {$duration}s" );
} catch ( Exception $e ) {
++$this->import_stats['errors_encountered'];
$duration = microtime( true ) - $start_time;
wc_get_logger()->error(
"Exception importing product: {$product_name} after {$duration}s - " . $e->getMessage(),
array(
'source' => 'wc-migrator',
'exception' => $e,
)
);
return $this->create_error_result( 'exception', $e->getMessage(), $product_data );
}
}
/**
* Import a batch of products.
*
* @param array $products_data Array of mapped product data.
* @param array $source_data_batch Array of original source data for reference.
* @return array Batch import results.
*/
public function import_batch( array $products_data, array $source_data_batch = array() ): array {
$results = array();
$batch_stats = array(
'successful' => 0,
'failed' => 0,
'skipped' => 0,
);
$total_count = count( $products_data );
foreach ( $products_data as $index => $product_data ) {
$source_data = $source_data_batch[ $index ] ?? array();
$product_name = $product_data['name'] ?? 'Unknown Product';
$result = $this->import_product( $product_data, $source_data );
$results[] = $result;
if ( 'success' === $result['status'] ) {
if ( 'skipped' === $result['action'] ) {
++$batch_stats['skipped'];
} else {
++$batch_stats['successful'];
}
} else {
++$batch_stats['failed'];
}
if ( $this->progress_callback ) {
call_user_func( $this->progress_callback, $index + 1, $total_count, $product_name, $result );
}
}
return array(
'results' => $results,
'stats' => $batch_stats,
);
}
/**
* Get current import statistics.
*
* @return array Import statistics.
*/
public function get_import_stats(): array {
return $this->import_stats;
}
/**
* Reset import statistics.
*/
public function reset_stats(): void {
$this->import_stats = array(
'products_created' => 0,
'products_updated' => 0,
'products_skipped' => 0,
'images_processed' => 0,
'errors_encountered' => 0,
);
}
/**
* Get default import options.
*
* @return array Default options.
*/
private function get_default_options(): array {
return array(
'skip_existing' => false,
'update_existing' => true,
'import_images' => true,
'image_timeout' => self::DEFAULT_IMAGE_TIMEOUT,
'max_images_per_product' => self::MAX_IMAGES_PER_PRODUCT,
'skip_duplicate_images' => false,
'create_categories' => true,
'create_tags' => true,
'handle_variations' => true,
'assign_default_category' => false,
'dry_run' => false,
);
}
/**
* Validate product data before import.
*
* @param array $product_data Product data to validate.
* @return array Validation result.
*/
private function validate_product_data( array $product_data ): array {
$required_fields = array( 'name' );
$missing_fields = array();
foreach ( $required_fields as $field ) {
if ( empty( $product_data[ $field ] ) ) {
$missing_fields[] = $field;
}
}
if ( ! empty( $missing_fields ) ) {
return array(
'valid' => false,
'message' => 'Missing required fields: ' . implode( ', ', $missing_fields ),
);
}
return array( 'valid' => true );
}
/**
* Find existing product by various identifiers.
*
* @param array $product_data Mapped product data.
* @return int|null Existing product ID or null if not found.
*/
private function find_existing_product( array $product_data ): ?int {
if ( ! empty( $product_data['original_product_id'] ) ) {
$existing_posts = get_posts(
array(
'post_type' => 'product',
'post_status' => 'any', // Find regardless of status.
'meta_key' => '_original_product_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => $product_data['original_product_id'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'fields' => 'ids',
'numberposts' => 1,
)
);
if ( ! empty( $existing_posts ) ) {
return (int) $existing_posts[0];
}
}
if ( ! empty( $product_data['sku'] ) ) {
$product_id = wc_get_product_id_by_sku( $product_data['sku'] );
if ( $product_id ) {
return $product_id;
}
}
if ( ! empty( $product_data['slug'] ) ) {
$post = get_page_by_path( $product_data['slug'], OBJECT, 'product' );
if ( $post ) {
return $post->ID;
}
}
return null;
}
/**
* Determine product type from product data.
*
* @param array $product_data Product data.
* @return string Product type.
*/
private function determine_product_type( array $product_data ): string {
if ( isset( $product_data['is_variable'] ) ) {
return $product_data['is_variable'] ? 'variable' : 'simple';
}
if ( ! empty( $product_data['variations'] ) && count( $product_data['variations'] ) >= 1 ) {
return 'variable';
}
if ( ! empty( $product_data['attributes'] ) ) {
foreach ( $product_data['attributes'] as $attribute ) {
if ( ! empty( $attribute['is_variation'] ) || ! empty( $attribute['variation'] ) ) {
return 'variable';
}
}
}
return 'simple';
}
/**
* Get or create product object with proper type conversion handling.
*
* @param int|null $existing_product_id Existing product ID if updating.
* @param string $required_type Required product type.
* @return WC_Product|null Product object or null on failure.
*/
private function get_or_create_product_object( ?int $existing_product_id, string $required_type ): ?WC_Product {
if ( ! $existing_product_id ) {
return $this->create_product_object( $required_type );
}
$existing_product = wc_get_product( $existing_product_id );
if ( ! $existing_product ) {
return $this->create_product_object( $required_type );
}
$current_type = $existing_product->get_type();
if ( $current_type === $required_type ) {
return $existing_product;
}
wc_get_logger()->info(
"Converting product ID {$existing_product_id} from {$current_type} to {$required_type}",
array( 'source' => 'wc-migrator' )
);
switch ( $required_type ) {
case 'variable':
return new WC_Product_Variable( $existing_product_id );
case 'simple':
default:
return new WC_Product_Simple( $existing_product_id );
}
}
/**
* Create appropriate product object based on type.
*
* @param string $product_type Product type.
* @return WC_Product|null Product object or null on failure.
*/
private function create_product_object( string $product_type ): ?WC_Product {
switch ( $product_type ) {
case 'variable':
return new WC_Product_Variable();
case 'simple':
default:
return new WC_Product_Simple();
}
}
/**
* Set basic product properties common to all product types.
*
* @param WC_Product $product Product object.
* @param array $product_data Product data.
*/
private function set_basic_product_properties( WC_Product $product, array $product_data ): void {
$product->set_name( $product_data['name'] );
if ( ! empty( $product_data['slug'] ) ) {
$product->set_slug( $product_data['slug'] );
}
if ( ! empty( $product_data['description'] ) ) {
$product->set_description( $product_data['description'] );
}
if ( ! empty( $product_data['short_description'] ) ) {
$product->set_short_description( $product_data['short_description'] );
}
if ( ! empty( $product_data['status'] ) ) {
$product->set_status( $product_data['status'] );
}
if ( ! empty( $product_data['sku'] ) ) {
$product->set_sku( $product_data['sku'] );
}
if ( isset( $product_data['catalog_visibility'] ) ) {
$product->set_catalog_visibility( $product_data['catalog_visibility'] );
}
if ( ! empty( $product_data['date_created_gmt'] ) ) {
$product->set_date_created( $product_data['date_created_gmt'] );
}
if ( ! empty( $product_data['weight'] ) ) {
$product->set_weight( $product_data['weight'] );
}
if ( ! empty( $product_data['tax_status'] ) ) {
$product->set_tax_status( $product_data['tax_status'] );
}
if ( ! empty( $product_data['metafields'] ) ) {
foreach ( $product_data['metafields'] as $key => $value ) {
if ( ! empty( $key ) ) {
$product->add_meta_data( $key, $value, true );
}
}
}
if ( ! empty( $product_data['meta_data'] ) ) {
foreach ( $product_data['meta_data'] as $meta ) {
if ( ! empty( $meta['key'] ) ) {
$product->add_meta_data( $meta['key'], $meta['value'] ?? '', true );
}
}
}
}
/**
* Handle simple product specific data.
*
* @param WC_Product_Simple $product Simple product object.
* @param array $product_data Product data.
*/
private function handle_simple_product( WC_Product_Simple $product, array $product_data ): void {
if ( ! empty( $product_data['regular_price'] ) ) {
$product->set_regular_price( $product_data['regular_price'] );
$product->set_price( $product_data['regular_price'] );
}
if ( ! empty( $product_data['sale_price'] ) ) {
$product->set_sale_price( $product_data['sale_price'] );
$product->set_price( $product_data['sale_price'] );
}
if ( ! empty( $product_data['sku'] ) ) {
add_filter( 'wc_product_has_unique_sku', '__return_false', 999 );
$product->set_sku( $product_data['sku'] );
remove_filter( 'wc_product_has_unique_sku', '__return_false', 999 );
}
if ( isset( $product_data['manage_stock'] ) ) {
$product->set_manage_stock( $product_data['manage_stock'] );
}
if ( ! empty( $product_data['stock_quantity'] ) ) {
$product->set_stock_quantity( (int) $product_data['stock_quantity'] );
}
if ( ! empty( $product_data['stock_status'] ) ) {
$product->set_stock_status( $product_data['stock_status'] );
}
if ( array_key_exists( 'cost_of_goods', $product_data ) ) {
$cogs_is_enabled = FeaturesUtil::feature_is_enabled( 'cost_of_goods_sold' );
if ( $cogs_is_enabled ) {
$product->set_cogs_value( (float) $product_data['cost_of_goods'] );
} else {
$this->set_cogs_value_direct( $product, (float) $product_data['cost_of_goods'] );
}
}
}
/**
* Handle variable product specific data.
*
* @param WC_Product_Variable $product Variable product object.
* @param array $product_data Product data.
*/
private function handle_variable_product( WC_Product_Variable $product, array $product_data ): void {
$product->set_sku( '' );
$product->set_regular_price( '' );
$product->set_sale_price( '' );
$product->set_manage_stock( false );
$product->set_weight( '' );
$product->set_stock_quantity( null );
if ( ! empty( $product_data['attributes'] ) ) {
$this->setup_attributes( $product, $product_data['attributes'] );
}
$product_id = $product->save();
if ( ! empty( $product_data['variations'] ) && $this->import_options['handle_variations'] ) {
$this->sync_variations( $product, $product_data['variations'] );
}
}
/**
* Set product attributes.
*
* @param WC_Product $product Product object.
* @param array $attributes Attributes data.
*/
private function set_product_attributes( WC_Product $product, array $attributes ): void {
$product_attributes = array();
foreach ( $attributes as $attribute_data ) {
if ( empty( $attribute_data['name'] ) ) {
continue;
}
$attribute = new \WC_Product_Attribute();
$attribute->set_name( $attribute_data['name'] );
$attribute->set_options( $attribute_data['options'] ?? array() );
$attribute->set_variation( $attribute_data['is_variation'] ?? $attribute_data['variation'] ?? false );
$attribute->set_visible( $attribute_data['is_visible'] ?? $attribute_data['visible'] ?? true );
$product_attributes[] = $attribute;
}
$product->set_attributes( $product_attributes );
}
/**
* Sets up product attributes for variable products with global taxonomy creation.
*
* @param WC_Product_Variable $product The variable product object.
* @param array $attributes_data Standardized attribute data from mapper.
*/
private function setup_attributes( WC_Product_Variable $product, array $attributes_data ): void {
$woo_attributes = array();
$this->current_attribute_mapping = array();
foreach ( $attributes_data as $attribute_info ) {
$attr_name = $attribute_info['name'] ?? null;
$attr_options = $attribute_info['options'] ?? array();
if ( empty( $attr_name ) || empty( $attr_options ) ) {
continue;
}
$taxonomy_slug = sanitize_title( $attr_name );
$taxonomy_name = 'pa_' . $taxonomy_slug;
$attribute_id = 0;
if ( ! taxonomy_exists( $taxonomy_name ) ) {
$attribute_id = wc_create_attribute(
array(
'name' => $attr_name,
'slug' => $taxonomy_slug,
'type' => 'select',
'order_by' => 'menu_order',
'has_archives' => false,
)
);
if ( is_wp_error( $attribute_id ) ) {
wc_get_logger()->warning( "Failed to create attribute '{$attr_name}': " . $attribute_id->get_error_message(), array( 'source' => 'wc-migrator' ) );
continue;
}
register_taxonomy(
$taxonomy_name,
/**
* Filters the object types associated with the attribute taxonomy.
*
* @since 10.2.0
* @param array $object_types Array of object types.
*/
apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ),
/**
* Filters the arguments for registering the attribute taxonomy.
*
* @since 10.2.0
* @param array $args Array of taxonomy registration arguments.
*/
apply_filters(
'woocommerce_taxonomy_args_' . $taxonomy_name,
array(
'labels' => array(
'name' => $attr_name,
),
'hierarchical' => false,
'show_ui' => false,
'show_in_rest' => true,
'query_var' => true,
'rewrite' => false,
'public' => false,
)
)
);
} else {
$attribute_id = wc_attribute_taxonomy_id_by_name( $taxonomy_name );
}
$term_ids = array();
$term_slugs = array();
foreach ( $attr_options as $value ) {
$term_slug = sanitize_title( $value );
$term = get_term_by( 'slug', $term_slug, $taxonomy_name );
if ( ! $term ) {
$term_result = wp_insert_term( $value, $taxonomy_name, array( 'slug' => $term_slug ) );
if ( is_wp_error( $term_result ) ) {
wc_get_logger()->warning( "Failed to insert term '{$value}' (slug: {$term_slug}) into {$taxonomy_name}: " . $term_result->get_error_message(), array( 'source' => 'wc-migrator' ) );
continue;
}
$term_ids[] = $term_result['term_id'];
$term_slugs[] = $term_slug;
} else {
$term_ids[] = $term->term_id;
$term_slugs[] = $term->slug;
}
}
$woo_attribute = new \WC_Product_Attribute();
$woo_attribute->set_name( $taxonomy_name );
$woo_attribute->set_id( $attribute_id );
$woo_attribute->set_options( $term_ids );
$woo_attribute->set_position( $attribute_info['position'] ?? 0 );
$woo_attribute->set_visible( $attribute_info['is_visible'] ?? true );
$woo_attribute->set_variation( $attribute_info['is_variation'] ?? true );
$woo_attributes[] = $woo_attribute;
$this->current_attribute_mapping[ $attr_name ] = $taxonomy_name;
}
$product->set_attributes( $woo_attributes );
}
/**
* Creates or updates product variations with proper mapping and lookup.
*
* @param WC_Product_Variable $product The parent variable product.
* @param array $variations_data Standardized variation data from mapper.
*/
private function sync_variations( WC_Product_Variable $product, array $variations_data ): void {
$parent_product_id = $product->get_id();
$parent_original_id = $product->get_meta( '_original_product_id' );
$processed_variation_ids = array();
$variation_count = count( $variations_data );
wc_get_logger()->debug( "Syncing {$variation_count} variations for product ID {$parent_product_id}", array( 'source' => 'wc-migrator' ) );
$attribute_taxonomy_map = $this->current_attribute_mapping;
// Build fallback mapping from product attributes if current mapping is empty.
if ( empty( $attribute_taxonomy_map ) ) {
$product_attributes = $product->get_attributes();
foreach ( $product_attributes as $taxonomy => $attribute_obj ) {
if ( $attribute_obj->get_variation() ) {
$attribute_label = wc_attribute_label( $taxonomy, $product );
// Store mapping with both original case and lowercase for case-insensitive lookup.
$attribute_taxonomy_map[ $attribute_label ] = $taxonomy;
$attribute_taxonomy_map[ strtolower( $attribute_label ) ] = $taxonomy;
}
}
}
foreach ( $variations_data as $var_data ) {
$original_variant_id = $var_data['original_id'] ?? null;
if ( ! $original_variant_id ) {
wc_get_logger()->warning( 'Skipping variation: Missing original ID.', array( 'source' => 'wc-migrator' ) );
continue;
}
$variation_id = null;
$variation = null;
if ( isset( $this->migration_data['variations_mapping'][ $original_variant_id ] ) ) {
$_variation_id = $this->migration_data['variations_mapping'][ $original_variant_id ];
$_variation = wc_get_product( $_variation_id );
if ( $_variation instanceof WC_Product_Variation && $_variation->get_parent_id() === $parent_product_id ) {
$variation = $_variation;
$variation_id = $_variation_id;
} else {
unset( $this->migration_data['variations_mapping'][ $original_variant_id ] );
}
}
if ( ! $variation ) {
$query_args = array(
'post_parent' => $parent_product_id,
'post_type' => 'product_variation',
'numberposts' => 1,
'post_status' => 'any',
'meta_key' => '_original_variant_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_value' => $original_variant_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'fields' => 'ids',
);
$found_ids = get_posts( $query_args );
if ( ! empty( $found_ids ) ) {
$variation_id = $found_ids[0];
$variation = wc_get_product( $variation_id );
if ( ! ( $variation instanceof WC_Product_Variation ) ) {
wc_get_logger()->warning( "Found post ID {$variation_id} for original variant {$original_variant_id}, but it's not a WC_Product_Variation.", array( 'source' => 'wc-migrator' ) );
$variation = null;
$variation_id = null;
}
}
}
if ( ! $variation ) {
$variation = new WC_Product_Variation();
$variation->set_parent_id( $parent_product_id );
}
$variation->set_status( 'publish' );
$variation->set_menu_order( $var_data['menu_order'] ?? 0 );
$variation->set_regular_price( $var_data['regular_price'] ?? '' );
$variation->set_sale_price( $var_data['sale_price'] ?? '' );
if ( ! empty( $var_data['sku'] ) ) {
add_filter( 'wc_product_has_unique_sku', '__return_false', 999 );
$variation->set_sku( $var_data['sku'] );
remove_filter( 'wc_product_has_unique_sku', '__return_false', 999 );
}
$variation->set_manage_stock( $var_data['manage_stock'] ?? false );
$variation->set_stock_quantity( $var_data['stock_quantity'] ?? null );
$variation->set_stock_status( $var_data['stock_status'] ?? 'instock' );
$variation->set_weight( $var_data['weight'] ?? '' );
if ( ! empty( $var_data['tax_status'] ) ) {
$variation->set_tax_status( $var_data['tax_status'] );
}
$image_original_id = $var_data['image_original_id'] ?? null;
if ( $image_original_id && isset( $this->migration_data['images_mapping'][ $image_original_id ] ) ) {
$variation->set_image_id( $this->migration_data['images_mapping'][ $image_original_id ] );
} else {
$variation->set_image_id( '' );
}
$wc_variation_attributes = array();
if ( ! empty( $var_data['attributes'] ) && is_array( $var_data['attributes'] ) ) {
foreach ( $var_data['attributes'] as $attr_name => $attr_value ) {
if ( isset( $attribute_taxonomy_map[ $attr_name ] ) ) {
$taxonomy = $attribute_taxonomy_map[ $attr_name ];
$term_slug = sanitize_title( $attr_value );
$normalized_attribute_name = wc_variation_attribute_name( $taxonomy );
$wc_variation_attributes[ $normalized_attribute_name ] = $term_slug;
} else {
wc_get_logger()->warning( "Attribute taxonomy mapping not found for option '{$attr_name}' while processing variation {$original_variant_id}.", array( 'source' => 'wc-migrator' ) );
}
}
}
$variation->set_attributes( $wc_variation_attributes );
$variation->update_meta_data( '_original_variant_id', $original_variant_id );
if ( $parent_original_id ) {
$variation->update_meta_data( '_original_product_id', $parent_original_id );
}
$saved_variation_id = $variation->save();
if ( $saved_variation_id ) {
$processed_variation_ids[] = $saved_variation_id;
$this->migration_data['variations_mapping'][ $original_variant_id ] = $saved_variation_id;
if ( ! empty( $var_data['cost_of_goods'] ) ) {
update_post_meta( $saved_variation_id, '_cogs_total_value', (float) $var_data['cost_of_goods'] );
}
} else {
wc_get_logger()->error( "Failed to save variation for original variant {$original_variant_id}", array( 'source' => 'wc-migrator' ) );
}
}
WC_Product_Variable::sync( $parent_product_id );
$processed_count = count( $processed_variation_ids );
wc_get_logger()->debug( "Successfully synced {$processed_count}/{$variation_count} variations for product ID {$parent_product_id}", array( 'source' => 'wc-migrator' ) );
}
/**
* Create product variations (legacy method - keeping for backward compatibility).
*
* @param int $parent_id Parent product ID.
* @param array $variations Variations data.
*/
private function create_product_variations( int $parent_id, array $variations ): void {
$product = wc_get_product( $parent_id );
if ( $product instanceof WC_Product_Variable ) {
$this->sync_variations( $product, $variations );
}
}
/**
* Handle post-save operations like metadata and migration tracking.
*
* @param int $product_id Product ID.
* @param array $product_data Product data.
*/
private function handle_post_save_operations( int $product_id, array $product_data ): void {
if ( ! empty( $product_data['original_product_id'] ) ) {
update_post_meta( $product_id, '_original_product_id', $product_data['original_product_id'] );
}
if ( ! empty( $product_data['original_url'] ) ) {
update_post_meta( $product_id, '_original_url', $product_data['original_url'] );
}
update_post_meta( $product_id, '_migration_data', $this->migration_data );
if ( ! empty( $product_data['metafields'] ) ) {
$this->update_seo_meta( $product_id, $product_data['metafields'], $product_data );
}
}
/**
* Set product taxonomies (categories, tags, brand) before product save.
*
* @param WC_Product $product The product object.
* @param array $product_data Standardized data containing taxonomies.
*/
private function set_product_taxonomies( WC_Product $product, array $product_data ): void {
$product_id = $product->get_id();
if ( ! $product_id ) {
$product_id = $product->save();
if ( ! $product_id ) {
wc_get_logger()->warning( 'Could not save product to set taxonomies.', array( 'source' => 'wc-migrator' ) );
return;
}
}
$taxonomies_to_set = array();
if ( isset( $product_data['categories'] ) && is_array( $product_data['categories'] ) && $this->import_options['create_categories'] ) {
$term_ids = $this->get_or_create_terms( $product_data['categories'], 'product_cat' );
if ( ! empty( $term_ids ) ) {
$taxonomies_to_set['product_cat'] = $term_ids;
} elseif ( $this->import_options['assign_default_category'] ) {
$default_cat_id = get_option( 'default_product_cat' );
if ( $default_cat_id ) {
$taxonomies_to_set['product_cat'] = array( $default_cat_id );
wc_get_logger()->info( "Assigned default category (ID: {$default_cat_id}) to product with no categories", array( 'source' => 'wc-migrator' ) );
}
} else {
wc_get_logger()->debug( 'Product has no categories and assign_default_category is disabled', array( 'source' => 'wc-migrator' ) );
}
}
if ( isset( $product_data['tags'] ) && is_array( $product_data['tags'] ) && $this->import_options['create_tags'] ) {
$term_ids = $this->get_or_create_terms( $product_data['tags'], 'product_tag' );
if ( ! empty( $term_ids ) ) {
$taxonomies_to_set['product_tag'] = $term_ids;
}
}
if ( ! empty( $product_data['brand']['name'] ) && taxonomy_exists( 'product_brand' ) ) {
$brand_data = array( $product_data['brand'] );
$term_ids = $this->get_or_create_terms( $brand_data, 'product_brand' );
if ( ! empty( $term_ids ) ) {
$taxonomies_to_set['product_brand'] = $term_ids;
}
}
foreach ( $taxonomies_to_set as $taxonomy => $ids ) {
wp_set_object_terms( $product_id, $ids, $taxonomy, false );
}
}
/**
* Helper to get or create term IDs for a given taxonomy.
*
* @param array $terms_data Array of ['name' => ..., 'slug' => ...].
* @param string $taxonomy Taxonomy slug.
* @return array Array of term IDs.
*/
private function get_or_create_terms( array $terms_data, string $taxonomy ): array {
$term_ids = array();
foreach ( $terms_data as $term_info ) {
$term_name = $term_info['name'] ?? null;
$term_slug = $term_info['slug'] ?? sanitize_title( $term_name );
if ( empty( $term_name ) || empty( $term_slug ) ) {
continue;
}
$term = get_term_by( 'slug', $term_slug, $taxonomy );
if ( ! $term ) {
$term_result = wp_insert_term( $term_name, $taxonomy, array( 'slug' => $term_slug ) );
if ( is_wp_error( $term_result ) ) {
wc_get_logger()->warning( "Failed to insert term '{$term_name}' (slug: {$term_slug}) into {$taxonomy}: " . $term_result->get_error_message(), array( 'source' => 'wc-migrator' ) );
continue;
}
$term_ids[] = $term_result['term_id'];
} else {
$term_ids[] = $term->term_id;
}
}
return array_unique( $term_ids );
}
/**
* Handle product images using product object methods.
*
* @param WC_Product $product The product object.
* @param array $images_data Standardized image data from mapper.
*/
private function handle_product_images( WC_Product $product, array $images_data ): void {
if ( empty( $images_data ) ) {
return;
}
$gallery_ids = array();
$featured_id = null;
$product_id = $product->get_id();
$processed_count = 0;
foreach ( $images_data as $index => $image ) {
if ( $processed_count >= $this->import_options['max_images_per_product'] ) {
break;
}
$original_id = $image['original_id'] ?? null;
$image_url = $image['src'] ?? null;
$image_alt = $image['alt'] ?? '';
$is_featured = $image['is_featured'] ?? ( 0 === $index );
if ( empty( $original_id ) || empty( $image_url ) ) {
wc_get_logger()->warning( 'Skipping image: Missing original ID or URL.', array( 'source' => 'wc-migrator' ) );
continue;
}
if ( isset( $this->migration_data['images_mapping'][ $original_id ] ) && wp_attachment_is_image( $this->migration_data['images_mapping'][ $original_id ] ) ) {
$attachment_id = $this->migration_data['images_mapping'][ $original_id ];
} else {
if ( ! $product_id ) {
$product_id = $product->save();
if ( ! $product_id ) {
wc_get_logger()->warning( "Skipping image upload {$original_id}: Could not get product ID before sideloading.", array( 'source' => 'wc-migrator' ) );
continue;
}
}
$start_time = microtime( true );
$image_desc = $image_alt ? $image_alt : $product->get_name();
$attachment_id = $this->import_image( $image_url, $image_alt, $product_id );
$duration = microtime( true ) - $start_time;
if ( is_wp_error( $attachment_id ) ) {
wc_get_logger()->error( "Error uploading {$image_url}: " . $attachment_id->get_error_message() . " (Duration: {$duration}s)", array( 'source' => 'wc-migrator' ) );
continue;
}
if ( ! $attachment_id ) {
wc_get_logger()->warning( "Image upload failed for {$image_url} (Duration: {$duration}s)", array( 'source' => 'wc-migrator' ) );
continue;
}
$this->migration_data['images_mapping'][ $original_id ] = $attachment_id;
if ( $image_alt ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $image_alt );
}
}
if ( $is_featured ) {
$featured_id = $attachment_id;
} else {
$gallery_ids[] = $attachment_id;
}
++$processed_count;
++$this->import_stats['images_processed'];
}
if ( $featured_id ) {
$product->set_image_id( $featured_id );
}
if ( ! empty( $gallery_ids ) ) {
$product->set_gallery_image_ids( array_unique( $gallery_ids ) );
}
}
/**
* Import image from URL with mapping optimization.
*
* @param string $image_url Image URL.
* @param string $alt_text Alt text for the image.
* @param string|null $original_id Original platform image ID.
* @param int $product_id Product ID for sideloading.
* @return int|null Attachment ID or null on failure.
*/
private function import_image_with_mapping( string $image_url, string $alt_text = '', ?string $original_id = null, int $product_id = 0 ): ?int {
if ( $original_id && isset( $this->migration_data['images_mapping'][ $original_id ] ) ) {
$attachment_id = $this->migration_data['images_mapping'][ $original_id ];
if ( wp_attachment_is_image( $attachment_id ) ) {
return $attachment_id;
} else {
unset( $this->migration_data['images_mapping'][ $original_id ] );
}
}
$start_time = microtime( true );
$attachment_id = $this->import_image( $image_url, $alt_text, $product_id );
$duration = microtime( true ) - $start_time;
if ( $attachment_id && $original_id ) {
$this->migration_data['images_mapping'][ $original_id ] = $attachment_id;
}
if ( $attachment_id ) {
$message = sprintf( 'Image uploaded successfully in %.2fs: %s -> %d', $duration, $image_url, $attachment_id );
if ( $this->import_options['verbose'] ?? false ) {
\WP_CLI::log( $message );
}
wc_get_logger()->info( $message, array( 'source' => 'wc-migrator-images' ) );
} else {
$message = sprintf( 'Image upload failed in %.2fs: %s', $duration, $image_url );
if ( $this->import_options['verbose'] ?? false ) {
\WP_CLI::warning( $message );
}
wc_get_logger()->error( $message, array( 'source' => 'wc-migrator-images' ) );
}
return $attachment_id;
}
/**
* Import image from URL.
*
* @param string $image_url Image URL.
* @param string $alt_text Alt text for the image.
* @param int $product_id Product ID for sideloading.
* @return int|null Attachment ID or null on failure.
*/
private function import_image( string $image_url, string $alt_text = '', int $product_id = 0 ): ?int {
if ( $this->import_options['dry_run'] ) {
return null;
}
if ( ! $this->import_options['skip_duplicate_images'] ) {
$existing_attachment = $this->get_attachment_by_url( $image_url );
if ( $existing_attachment ) {
return $existing_attachment;
}
}
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
add_filter( 'http_request_timeout', array( $this, 'set_image_download_timeout' ) );
add_filter( 'http_request_args', array( $this, 'optimize_http_request_args' ) );
add_filter( 'image_sideload_extensions', array( $this, 'add_avif_support_to_sideload' ) );
try {
$attachment_id = media_sideload_image( $image_url, $product_id, null, 'id' );
if ( is_wp_error( $attachment_id ) ) {
$message = sprintf( 'Image import failed for URL %s: %s', $image_url, $attachment_id->get_error_message() );
if ( $this->import_options['verbose'] ?? false ) {
\WP_CLI::warning( $message );
}
wc_get_logger()->error( $message, array( 'source' => 'wc-migrator-images' ) );
return null;
}
if ( $alt_text ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text );
}
return $attachment_id;
} finally {
remove_filter( 'http_request_timeout', array( $this, 'set_image_download_timeout' ) );
remove_filter( 'http_request_args', array( $this, 'optimize_http_request_args' ) );
remove_filter( 'image_sideload_extensions', array( $this, 'add_avif_support_to_sideload' ) );
}
}
/**
* Set HTTP timeout for image downloads.
*
* @return int Modified timeout.
*/
public function set_image_download_timeout(): int {
return $this->import_options['image_timeout'];
}
/**
* Optimize HTTP request arguments for faster image downloads.
*
* @param array $args HTTP request arguments.
* @return array Optimized arguments.
*/
public function optimize_http_request_args( array $args ): array {
$args['redirection'] = 3;
$args['timeout'] = $this->import_options['image_timeout'] ?? 30;
return $args;
}
/**
* Add AVIF support to image sideload extensions.
*
* @param array $allowed_extensions Array of allowed file extensions.
* @return array Modified array with AVIF support.
*/
public function add_avif_support_to_sideload( array $allowed_extensions ): array {
if ( ! in_array( 'avif', $allowed_extensions, true ) ) {
$allowed_extensions[] = 'avif';
}
return $allowed_extensions;
}
/**
* Get existing attachment by URL.
*
* @param string $image_url Image URL.
* @return int|null Attachment ID or null if not found.
*/
private function get_attachment_by_url( string $image_url ): ?int {
global $wpdb;
$basename = wp_basename( $image_url );
$attachment_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value LIKE %s",
'%' . $wpdb->esc_like( $basename )
)
);
return $attachment_id ? (int) $attachment_id : null;
}
/**
* Create success result array.
*
* @param string $action Action performed (created, updated, skipped).
* @param int $product_id Product ID.
* @param string $message Success message.
* @return array Success result.
*/
private function create_success_result( string $action, int $product_id, string $message ): array {
return array(
'status' => 'success',
'action' => $action,
'product_id' => $product_id,
'message' => $message,
);
}
/**
* Updates SEO meta fields if Yoast SEO is active.
*
* @param int $product_id The product ID.
* @param array $metafields Key-value array of metafields from standardized data.
* @param array $product_data Full product data for fallbacks.
*/
private function update_seo_meta( int $product_id, array $metafields, array $product_data ): void {
if ( ! defined( 'WPSEO_VERSION' ) ) {
return;
}
$seo_title = $metafields['global_title_tag'] ?? null;
$seo_description = $metafields['global_description_tag'] ?? null;
$final_seo_title = $seo_title ? $seo_title : ( $product_data['name'] ?? '' );
$fallback_desc = $product_data['description'] ? $product_data['description'] : ( $product_data['short_description'] ?? '' );
$final_seo_description = $seo_description ? $seo_description : wp_strip_all_tags( $fallback_desc );
$current_title = get_post_meta( $product_id, '_yoast_wpseo_title', true );
if ( $current_title !== $final_seo_title && ! empty( $final_seo_title ) ) {
update_post_meta( $product_id, '_yoast_wpseo_title', $final_seo_title );
}
$current_desc = get_post_meta( $product_id, '_yoast_wpseo_metadesc', true );
if ( $current_desc !== $final_seo_description && ! empty( $final_seo_description ) ) {
$truncated_desc = mb_substr( $final_seo_description, 0, 160 );
update_post_meta( $product_id, '_yoast_wpseo_metadesc', $truncated_desc );
}
}
/**
* Set COGS value directly using meta data.
*
* @param WC_Product $product The product object.
* @param float $cogs_value The COGS value to set.
*/
private function set_cogs_value_direct( WC_Product $product, float $cogs_value ): void {
$product->update_meta_data( '_cogs_total_value', $cogs_value );
}
/**
* Create error result array.
*
* @param string $error_code Error code.
* @param string $message Error message.
* @param array $product_data Product data that failed.
* @return array Error result.
*/
private function create_error_result( string $error_code, string $message, array $product_data ): array {
return array(
'status' => 'error',
'error_code' => $error_code,
'message' => $message,
'product_data' => $product_data,
);
}
}