Automattic\WooCommerce\Internal\CLI\Migrator\Platforms\Shopify

ShopifyMapper{}WC 1.0└─ PlatformMapperInterface

ShopifyMapper class.

This class is responsible for transforming raw Shopify product data into a standardized format suitable for the WooCommerce Importer. Maps comprehensive product data including variants, images, taxonomies, and metadata from Shopify's GraphQL API response format.

No Hooks.

Usage

$ShopifyMapper = new ShopifyMapper();
// use class methods

Methods

  1. public __construct( array $args = array() )
  2. public map_product_data( object $shopify_product )
  3. private get_converted_weight( $weight, $weight_unit )
  4. private get_default_product_fields()
  5. private get_mapped_categories( object $shopify_product )
  6. private get_mapped_tags( object $shopify_product )
  7. private get_woo_product_status( object $shopify_product )
  8. private is_variable_product( object $shopify_product )
  9. private map_basic_product_fields( object $shopify_product, bool $is_variable )
  10. private map_enhanced_status( object $shopify_product )
  11. private map_metafields( object $shopify_product )
  12. private map_product_classification( object $shopify_product )
  13. private map_product_images( object $shopify_product )
  14. private map_seo_fields( object $shopify_product )
  15. private map_simple_product_data( object $shopify_product )
  16. private map_variable_product_data( object $shopify_product, bool $is_variable )
  17. private should_process( string $field_key )

ShopifyMapper{} code WC 10.8.1

class ShopifyMapper implements PlatformMapperInterface {

	/**
	 * Shopify weight unit to standard unit mapping.
	 *
	 * @var array
	 */
	private const WEIGHT_UNIT_MAP = array(
		'GRAMS'     => 'g',
		'KILOGRAMS' => 'kg',
		'POUNDS'    => 'lb',
		'OUNCES'    => 'oz',
	);

	/**
	 * Weight conversion factors between units.
	 * Structure: [from_unit][to_unit] = factor
	 *
	 * @var array
	 */
	private const WEIGHT_CONVERSION_FACTORS = array(
		'kg' => array(
			'kg' => 1,
			'g'  => 1000,
			'lb' => 2.20462,
			'oz' => 35.274,
		),
		'g'  => array(
			'kg' => 0.001,
			'g'  => 1,
			'lb' => 0.00220462,
			'oz' => 0.035274,
		),
		'lb' => array(
			'kg' => 0.453592,
			'g'  => 453.592,
			'lb' => 1,
			'oz' => 16,
		),
		'oz' => array(
			'kg' => 0.0283495,
			'g'  => 28.3495,
			'lb' => 0.0625,
			'oz' => 1,
		),
	);

	/**
	 * Fields to process during mapping.
	 *
	 * @var array
	 */
	private $fields_to_process = array();

	/**
	 * Constructor.
	 *
	 * @param array $args Optional arguments including 'fields' array for selective processing.
	 */
	public function __construct( array $args = array() ) {
		$this->fields_to_process = $args['fields'] ?? $this->get_default_product_fields();
	}

	/**
	 * Maps raw Shopify product data to a standardized array format.
	 *
	 * @param object $shopify_product The raw Shopify product node from GraphQL.
	 * @return array Standardized data array for WooCommerce_Product_Importer.
	 */
	public function map_product_data( object $shopify_product ): array {
		$is_variable = $this->is_variable_product( $shopify_product );

		$wc_data = $this->map_basic_product_fields( $shopify_product, $is_variable );

		// Map simple product data (for non-variable products).
		if ( ! $is_variable ) {
			$simple_data = $this->map_simple_product_data( $shopify_product );
			$wc_data     = array_merge( $wc_data, $simple_data );
		}

		// Map product images.
		$wc_data['images'] = $this->map_product_images( $shopify_product );

		// Map metafields and SEO data.
		$wc_data['metafields'] = $this->map_metafields( $shopify_product );

		// Map variable product data (attributes and variations).
		$variable_data = $this->map_variable_product_data( $shopify_product, $is_variable );
		$wc_data       = array_merge( $wc_data, $variable_data );

		return $wc_data;
	}

	/**
	 * Checks if a product is a variable product.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return bool True if the product is a variable product, false otherwise.
	 */
	private function is_variable_product( object $shopify_product ): bool {
		return isset( $shopify_product->variants->edges ) && count( $shopify_product->variants->edges ) > 1;
	}

	/**
	 * Converts the Shopify product status into WooCommerce product status.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return string The WooCommerce product status.
	 */
	private function get_woo_product_status( object $shopify_product ): string {
		$woo_product_status = 'draft';
		if ( 'ACTIVE' === $shopify_product->status ) {
			$woo_product_status = 'publish';
		}
		return $woo_product_status;
	}

	/**
	 * Maps enhanced publication status fields from Shopify.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Enhanced status data.
	 */
	private function map_enhanced_status( object $shopify_product ): array {
		$status_data = array();

		// Publication date.
		if ( property_exists( $shopify_product, 'publishedAt' ) && $shopify_product->publishedAt ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			$status_data['date_published_gmt'] = $shopify_product->publishedAt; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		}

		// Available for sale flag.
		if ( property_exists( $shopify_product, 'availableForSale' ) ) {
			$status_data['available_for_sale'] = $shopify_product->availableForSale; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		}

		return $status_data;
	}

	/**
	 * Maps product classification fields from Shopify.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Product classification data.
	 */
	private function map_product_classification( object $shopify_product ): array {
		$classification = array();

		// Product type - check both camelCase and snake_case for compatibility.
		$product_type = null;
		if ( property_exists( $shopify_product, 'productType' ) && $shopify_product->productType ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			$product_type = $shopify_product->productType; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		} elseif ( property_exists( $shopify_product, 'product_type' ) && $shopify_product->product_type ) {
			$product_type = $shopify_product->product_type;
		}

		if ( $product_type ) {
			$classification['product_type'] = array(
				'name' => $product_type,
				'slug' => sanitize_title( $product_type ),
			);
		}

		// Standard category.
		if ( property_exists( $shopify_product, 'category' ) && is_object( $shopify_product->category ) ) {
			$classification['standard_category'] = array(
				'name' => $shopify_product->category->name ?? '',
				'slug' => sanitize_title( $shopify_product->category->name ?? '' ),
			);
		}

		// Gift card detection - check both camelCase and snake_case for compatibility.
		if ( property_exists( $shopify_product, 'isGiftCard' ) ) {
			$classification['is_gift_card'] = $shopify_product->isGiftCard; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		} elseif ( property_exists( $shopify_product, 'is_gift_card' ) ) {
			$classification['is_gift_card'] = $shopify_product->is_gift_card;
		}

		if ( property_exists( $shopify_product, 'requiresSellingPlan' ) ) {
			$classification['requires_subscription'] = $shopify_product->requiresSellingPlan; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		} elseif ( property_exists( $shopify_product, 'requires_selling_plan' ) ) {
			$classification['requires_subscription'] = $shopify_product->requires_selling_plan;
		}

		return $classification;
	}

	/**
	 * Maps SEO fields from Shopify product data.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array SEO metafields data.
	 */
	private function map_seo_fields( object $shopify_product ): array {
		$seo_data = array();

		if ( property_exists( $shopify_product, 'seo' ) && is_object( $shopify_product->seo ) ) {
			if ( ! empty( $shopify_product->seo->title ) ) {
				$seo_data['global_title_tag'] = $shopify_product->seo->title;
			}
			if ( ! empty( $shopify_product->seo->description ) ) {
				$seo_data['global_description_tag'] = $shopify_product->seo->description;
			}
		}

		return $seo_data;
	}

	/**
	 * Gets mapped WooCommerce product categories from Shopify collections.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Mapped category data.
	 */
	private function get_mapped_categories( object $shopify_product ): array {
		$categories = array();
		if ( ! property_exists( $shopify_product, 'collections' ) || empty( $shopify_product->collections->edges ) ) {
			return $categories;
		}

		foreach ( $shopify_product->collections->edges as $collection_edge ) {
			$collection_node = $collection_edge->node;
			$categories[]    = array(
				'name' => wc_clean( $collection_node->title ),
				'slug' => sanitize_title( $collection_node->handle ),
			);
		}

		return $categories;
	}

	/**
	 * Gets mapped WooCommerce product tags from Shopify tags.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Mapped tag data.
	 */
	private function get_mapped_tags( object $shopify_product ): array {
		$tags = array();
		if ( empty( $shopify_product->tags ) ) {
			return $tags;
		}

		foreach ( $shopify_product->tags as $tag ) {
			$trimmed_tag = trim( $tag );
			if ( ! empty( $trimmed_tag ) ) {
				$tags[] = array(
					'name' => wc_clean( $trimmed_tag ),
					'slug' => sanitize_title( $trimmed_tag ),
				);
			}
		}
		return $tags;
	}

	/**
	 * Converts weight based on Shopify weight unit to store's weight unit.
	 *
	 * @param float|null  $weight      The weight value from Shopify.
	 * @param string|null $weight_unit The weight unit from Shopify.
	 * @return float|null The converted weight, or null if input is invalid/zero.
	 */
	private function get_converted_weight( $weight, $weight_unit ): ?float {
		if ( null === $weight || null === $weight_unit || (float) $weight <= 0 ) {
			return null;
		}

		$shopify_unit_key = self::WEIGHT_UNIT_MAP[ $weight_unit ] ?? null;

		if ( ! $shopify_unit_key ) {
			return (float) $weight;
		}

		$store_weight_unit = get_option( 'woocommerce_weight_unit' );

		if ( 'lbs' === $store_weight_unit ) {
			$store_weight_unit = 'lb';
		}

		if ( $shopify_unit_key === $store_weight_unit ) {
			return (float) $weight;
		}

		// Use wc_get_weight for conversion if possible.
		if ( function_exists( 'wc_get_weight' ) ) {
			$converted = wc_get_weight( (float) $weight, $store_weight_unit, $shopify_unit_key );
			return is_numeric( $converted ) ? (float) $converted : null;
		}

		// Fallback manual conversion using class constants.
		if ( ! isset( self::WEIGHT_CONVERSION_FACTORS[ $shopify_unit_key ][ $store_weight_unit ] ) ) {
			return (float) $weight;
		}

		return (float) $weight * self::WEIGHT_CONVERSION_FACTORS[ $shopify_unit_key ][ $store_weight_unit ];
	}


	/**
	 * Checks if a specific field should be processed based on constructor args.
	 *
	 * @param string $field_key The field key.
	 * @return bool True if the field should be processed.
	 */
	private function should_process( string $field_key ): bool {
		if ( empty( $this->fields_to_process ) ) {
			return true;
		}
		return in_array( $field_key, $this->fields_to_process, true );
	}

	/**
	 * Maps basic product fields from Shopify to WooCommerce format.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @param bool   $is_variable     Whether this is a variable product.
	 * @return array Basic product field mappings.
	 */
	private function map_basic_product_fields( object $shopify_product, bool $is_variable ): array {
		$basic_data = array();

		$basic_data['is_variable']         = $is_variable;
		$basic_data['original_product_id'] = ! empty( $shopify_product->id ) ? basename( $shopify_product->id ) : null;

		// Basic Product Fields.
		$basic_data['name']              = wc_clean( $shopify_product->title );
		$basic_data['slug']              = sanitize_title( $shopify_product->handle );
		$basic_data['description']       = wp_kses_post( $shopify_product->descriptionHtml ?? '' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		$basic_data['short_description'] = wp_kses_post( $shopify_product->descriptionPlainSummary ?? '' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		$basic_data['status']            = $this->get_woo_product_status( $shopify_product );
		$basic_data['date_created_gmt']  = $shopify_product->createdAt; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.

		// Enhanced date handling.
		if ( property_exists( $shopify_product, 'updatedAt' ) ) {
			$basic_data['date_modified_gmt'] = $shopify_product->updatedAt; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		}

		// Catalog Visibility & Original URL.
		$basic_data['catalog_visibility'] = 'visible';
		$basic_data['original_url']       = null;
		if ( property_exists( $shopify_product, 'onlineStoreUrl' ) ) {
			if ( null === $shopify_product->onlineStoreUrl ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				$basic_data['catalog_visibility'] = 'hidden';
			} else {
				$basic_data['original_url'] = $shopify_product->onlineStoreUrl; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			}
		}

		$enhanced_status = $this->map_enhanced_status( $shopify_product );
		$basic_data      = array_merge( $basic_data, $enhanced_status );

		// Taxonomies.
		$basic_data['categories'] = $this->get_mapped_categories( $shopify_product );
		$basic_data['tags']       = $this->get_mapped_tags( $shopify_product );

		// Enhanced product classification.
		$classification = $this->map_product_classification( $shopify_product );
		$basic_data     = array_merge( $basic_data, $classification );

		// Brand (Vendor).
		$brand_name          = $shopify_product->vendor ?? null;
		$basic_data['brand'] = $brand_name ? array(
			'name' => wc_clean( $brand_name ),
			'slug' => sanitize_title( $brand_name ),
		) : null;

		return $basic_data;
	}

	/**
	 * Maps simple product data (price, SKU, stock, weight) from Shopify variant.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Simple product data mappings.
	 */
	private function map_simple_product_data( object $shopify_product ): array {
		$simple_data = array();

		if ( ! empty( $shopify_product->variants->edges ) ) {
			$variant_node = $shopify_product->variants->edges[0]->node;

			if ( $this->should_process( 'price' ) ) {
				if ( $variant_node->compareAtPrice && $variant_node->compareAtPrice > $variant_node->price ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					$simple_data['sale_price']    = $variant_node->price;
					$simple_data['regular_price'] = $variant_node->compareAtPrice; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				} else {
					$simple_data['sale_price']    = null;
					$simple_data['regular_price'] = $variant_node->price;
				}
			}

			if ( $this->should_process( 'sku' ) ) {
				$simple_data['sku'] = wc_clean( $variant_node->sku );
			}

			if ( $this->should_process( 'stock' ) ) {
				$manage_stock                  = property_exists( $variant_node, 'inventoryItem' ) && $variant_node->inventoryItem->tracked; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				$simple_data['manage_stock']   = $manage_stock;
				$stock_quantity                = $variant_node->inventoryQuantity ?? 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				$allow_oversell                = $manage_stock && 'CONTINUE' === $variant_node->inventoryPolicy; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				$simple_data['stock_status']   = ( $stock_quantity > 0 || $allow_oversell ) ? 'instock' : 'outofstock';
				$simple_data['stock_quantity'] = $stock_quantity;
			}

			if ( $this->should_process( 'weight' ) ) {
				$weight_data = null;
				if ( property_exists( $variant_node, 'inventoryItem' ) && is_object( $variant_node->inventoryItem ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					property_exists( $variant_node->inventoryItem, 'measurement' ) && is_object( $variant_node->inventoryItem->measurement ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					property_exists( $variant_node->inventoryItem->measurement, 'weight' ) && is_object( $variant_node->inventoryItem->measurement->weight ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				) {
					$weight_data = $variant_node->inventoryItem->measurement->weight; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				}
				$weight                = $weight_data ? $weight_data->value : null;
				$weight_unit           = $weight_data ? $weight_data->unit : null;
				$simple_data['weight'] = $this->get_converted_weight( $weight, $weight_unit );
			}

			if ( property_exists( $variant_node, 'inventoryItem' ) && is_object( $variant_node->inventoryItem ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				property_exists( $variant_node->inventoryItem, 'unitCost' ) && is_object( $variant_node->inventoryItem->unitCost ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			) {
				$simple_data['cost_of_goods'] = $variant_node->inventoryItem->unitCost->amount; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			}

			if ( property_exists( $variant_node, 'taxable' ) ) {
				$simple_data['tax_status'] = $variant_node->taxable ? 'taxable' : 'none';
			}

			$simple_data['original_variant_id'] = ! empty( $variant_node->id ) ? basename( $variant_node->id ) : null;

		} else {
			$simple_data['sku']            = null;
			$simple_data['regular_price']  = null;
			$simple_data['sale_price']     = null;
			$simple_data['stock_quantity'] = null;
			$simple_data['manage_stock']   = false;
			$simple_data['stock_status']   = 'instock';
			$simple_data['weight']         = null;

			if ( property_exists( $shopify_product, 'taxable' ) ) {
				$simple_data['tax_status'] = $shopify_product->taxable ? 'taxable' : 'none';
			}

			$simple_data['original_variant_id'] = null;
		}

		return $simple_data;
	}

	/**
	 * Maps variable product data (attributes and variations) from Shopify.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @param bool   $is_variable     Whether this is a variable product.
	 * @return array Variable product data mappings.
	 */
	private function map_variable_product_data( object $shopify_product, bool $is_variable ): array {
		$variable_data = array();

		// Attributes (Variable Only).
		$variable_data['attributes'] = array();
		if ( $is_variable && property_exists( $shopify_product, 'options' ) && ! empty( $shopify_product->options ) ) {
			foreach ( $shopify_product->options as $option ) {
				$variable_data['attributes'][] = array(
					'name'         => wc_clean( $option->name ),
					'options'      => array_map( 'wc_clean', $option->values ),
					'position'     => $option->position,
					'is_visible'   => true,
					'is_variation' => true,
				);
			}
		}

		// Variations (Variable Only).
		$variable_data['variations'] = array();
		if ( $is_variable && property_exists( $shopify_product, 'variants' ) && ! empty( $shopify_product->variants->edges ) ) {
			foreach ( $shopify_product->variants->edges as $variant_edge ) {
				$variant_node                  = $variant_edge->node;
				$variation_data                = array();
				$variation_data['original_id'] = ! empty( $variant_node->id ) ? basename( $variant_node->id ) : null;

				if ( $this->should_process( 'price' ) ) {
					if ( $variant_node->compareAtPrice && (float) $variant_node->compareAtPrice > (float) $variant_node->price ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
						$variation_data['regular_price'] = $variant_node->compareAtPrice; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
						$variation_data['sale_price']    = $variant_node->price;
					} else {
						$variation_data['regular_price'] = $variant_node->price;
						$variation_data['sale_price']    = null;
					}
				}

				if ( $this->should_process( 'sku' ) ) {
					$variation_data['sku'] = wc_clean( $variant_node->sku ?? '' );
				}

				if ( $this->should_process( 'stock' ) ) {
					$manage_stock                     = property_exists( $variant_node, 'inventoryItem' ) && $variant_node->inventoryItem->tracked; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					$variation_data['manage_stock']   = $manage_stock;
					$stock_quantity                   = $variant_node->inventoryQuantity ?? 0; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					$allow_oversell                   = $manage_stock && 'CONTINUE' === $variant_node->inventoryPolicy; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					$variation_data['stock_status']   = ( $stock_quantity > 0 || $allow_oversell ) ? 'instock' : 'outofstock';
					$variation_data['stock_quantity'] = $stock_quantity;
				}

				if ( $this->should_process( 'weight' ) ) {
					$weight_data = null;
					if ( property_exists( $variant_node, 'inventoryItem' ) && is_object( $variant_node->inventoryItem ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
						property_exists( $variant_node->inventoryItem, 'measurement' ) && is_object( $variant_node->inventoryItem->measurement ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
						property_exists( $variant_node->inventoryItem->measurement, 'weight' ) && is_object( $variant_node->inventoryItem->measurement->weight ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					) {
						$weight_data = $variant_node->inventoryItem->measurement->weight; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					}
					$weight                   = $weight_data ? $weight_data->value : null;
					$weight_unit              = $weight_data ? $weight_data->unit : null;
					$variation_data['weight'] = $this->get_converted_weight( $weight, $weight_unit );
				}

				if ( property_exists( $variant_node, 'inventoryItem' ) && is_object( $variant_node->inventoryItem ) && // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
					property_exists( $variant_node->inventoryItem, 'unitCost' ) && is_object( $variant_node->inventoryItem->unitCost ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				) {
					$variation_data['cost_of_goods'] = $variant_node->inventoryItem->unitCost->amount; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
				}

				if ( property_exists( $variant_node, 'taxable' ) ) {
					$variation_data['tax_status'] = $variant_node->taxable ? 'taxable' : 'none';
				}

				if ( $this->should_process( 'attributes' ) ) {
					$variation_data['attributes'] = array();
					if ( ! empty( $variant_node->selectedOptions ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
						foreach ( $variant_node->selectedOptions as $selectedOption ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- GraphQL uses camelCase.
							$variation_data['attributes'][ wc_clean( $selectedOption->name ) ] = wc_clean( $selectedOption->value ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- GraphQL uses camelCase.
						}
					}
				}

				if ( $this->should_process( 'images' ) ) {
					$variation_data['image_original_id'] = null;
					if ( ! empty( $variant_node->media->edges ) ) {
						$variant_media_node = $variant_node->media->edges[0]->node ?? null;
						if ( $variant_media_node && property_exists( $variant_media_node, 'image' ) && is_object( $variant_media_node->image ) && ! empty( $variant_media_node->id ) ) {
							$variation_data['image_original_id'] = $variant_media_node->id;
						}
					}
				}

				// Menu Order / Position.
				$variation_data['menu_order'] = $variant_node->position;

				$variable_data['variations'][] = $variation_data;
			}
		}

		return $variable_data;
	}

	/**
	 * Maps product images from Shopify media data.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Product images data.
	 */
	private function map_product_images( object $shopify_product ): array {
		$images_data       = array();
		$featured_media_id = null;

		if ( ! empty( $shopify_product->featuredMedia ) && is_object( $shopify_product->featuredMedia ) && ! empty( $shopify_product->featuredMedia->id ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
			$featured_media_id = $shopify_product->featuredMedia->id; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL uses camelCase.
		}

		if ( ! empty( $shopify_product->media->edges ) ) {
			foreach ( $shopify_product->media->edges as $media_edge ) {
				$media_node = $media_edge->node;
				if ( property_exists( $media_node, 'image' ) && is_object( $media_node->image ) && ! empty( $media_node->id ) && ! empty( $media_node->image->url ) ) {
					$images_data[] = array(
						'original_id' => $media_node->id,
						'src'         => $media_node->image->url,
						'alt'         => $media_node->image->altText ?? null,
						'is_featured' => ( $media_node->id === $featured_media_id ),
					);
				}
			}
		}

		return $images_data;
	}

	/**
	 * Maps metafields and SEO data from Shopify product.
	 *
	 * @param object $shopify_product The Shopify product data.
	 * @return array Metafields data.
	 */
	private function map_metafields( object $shopify_product ): array {
		$metafields_data = array();

		if ( property_exists( $shopify_product, 'metafields' ) && ! empty( $shopify_product->metafields->edges ) ) {
			foreach ( $shopify_product->metafields->edges as $edge ) {
				$field_node              = $edge->node;
				$key                     = sprintf( '%s_%s', $field_node->namespace, $field_node->key );
				$metafields_data[ $key ] = $field_node->value;
			}
		}

		// Enhanced SEO mapping.
		$seo_data        = $this->map_seo_fields( $shopify_product );
		$metafields_data = array_merge( $metafields_data, $seo_data );

		return $metafields_data;
	}

	/**
	 * Gets the default product fields to process if not specified.
	 *
	 * @return array Default fields.
	 */
	private function get_default_product_fields(): array {
		return array(
			'title',
			'slug',
			'description',
			'short_description',
			'status',
			'date_created',
			'catalog_visibility',
			'category',
			'tag',
			'price',
			'sku',
			'stock',
			'weight',
			'brand',
			'images',
			'seo',
			'attributes',
		);
	}
}