Automattic\WooCommerce\Internal\CLI\Migrator\Platforms\Webflow
WebflowMapper{} │ WC 1.0└─ PlatformMapperInterface
Transforms a raw Webflow product+SKUs payload into the standardized array consumed by WooCommerceProductImporter.
Webflow's eCommerce model differs from Shopify's in several ways:
- Variants ("SKUs") live nested under the product in a single response.
- Variant options live in
product.fieldData['sku-properties']as an array
of `{ id, name, enum: [ { id, name, slug } ] }`. Each SKU's `sku-values`
maps property id => enum id, so the mapper must resolve ids to human
names before producing WC attributes / variation attributes.
- Prices are integer minor units (e.g. cents). The unit lives alongside.
- Categories are CMS item references that the fetcher pre-resolves onto
the item as `_resolved_categories` (an array of `{name, slug}`).
- Categories are CMS item references that the fetcher pre-resolves onto
- Images come from
main-image+more-imageson each SKU, plus`more-images` on the product itself. They must all live in a single `images[]` array so the importer can build the original_id => attachment map that variations rely on.
No Hooks.
Usage
$WebflowMapper = new WebflowMapper(); // use class methods
Methods
- public __construct( array $args = array() )
- public map_product_data( object $platform_data )
- private add_unique_image( array &$images, array &$by_url, array $entry )
- private build_images( object $field_data, array $skus )
- private extract_dimensions( object $sku_field )
- private extract_field_data( object $product )
- private extract_prices( object $sku_field )
- private extract_product( object $platform_data )
- private extract_sku_properties( object $field_data )
- private extract_skus( object $platform_data )
- private extract_stock( object $sku_field )
- private extract_weight( object $sku_field )
- private get_currency_decimals( string $currency )
- private map_basic_fields( object $product, object $field_data, bool $is_variable )
- private map_categories( object $platform_data )
- private map_seo( object $field_data )
- private map_simple_data( array $skus )
- private map_status( object $product )
- private map_variable_data( array $properties, array $skus, array $images )
- private normalize_image_object( $img )
- private normalize_weight_unit( string $unit )
- private price_to_decimal( $money )
- private resolve_variation_attributes( object $sku_field, array $property_lookup )
- private should_process( string $field_key )
WebflowMapper{} WebflowMapper{} code WC 11.0.0
class WebflowMapper implements PlatformMapperInterface {
/**
* Fields to process during mapping (selected via --fields/--exclude-fields).
*
* @var array
*/
private array $fields_to_process = array();
/**
* Map of ISO currency code => minor-unit count, lazily built from core's locale-info.
*
* @var array<string,int>|null
*/
private ?array $currency_decimals = null;
/**
* Constructor.
*
* @param array $args Optional arguments. Recognized keys:
* - 'fields': array of field keys to process. Empty/missing means all.
*/
public function __construct( array $args = array() ) {
$this->fields_to_process = $args['fields'] ?? array();
}
/**
* Maps a raw Webflow product+SKUs item into the importer's standardized array shape.
*
* @param object $platform_data Webflow product item: `{ product: { id, fieldData: ... }, skus: [...], _resolved_categories?: [...] }`.
* @return array Standardized data array.
*/
public function map_product_data( object $platform_data ): array {
$product = $this->extract_product( $platform_data );
$field_data = $this->extract_field_data( $product );
$skus = $this->extract_skus( $platform_data );
$properties = $this->extract_sku_properties( $field_data );
// Needs properties AND more than one SKU. A single-SKU product is imported as simple,
// intentionally dropping its lone option (a one-value attribute adds no variation choice).
$is_variable = ! empty( $properties ) && count( $skus ) > 1;
$wc_data = $this->map_basic_fields( $product, $field_data, $is_variable );
$wc_data['categories'] = $this->should_process( 'categories' ) ? $this->map_categories( $platform_data ) : array();
$wc_data['tags'] = array();
$images = $this->should_process( 'images' ) ? $this->build_images( $field_data, $skus ) : array();
$wc_data['images'] = $images;
if ( $is_variable ) {
$wc_data = array_merge( $wc_data, $this->map_variable_data( $properties, $skus, $images ) );
} else {
$wc_data = array_merge( $wc_data, $this->map_simple_data( $skus ) );
$wc_data['attributes'] = array();
$wc_data['variations'] = array();
}
$wc_data['metafields'] = $this->map_seo( $field_data );
return $wc_data;
}
/**
* Webflow list-products items wrap a `product` object and a `skus` array.
*
* Accept either shape (the wrapped item, or a bare product object) so tests
* and downstream callers can be lenient.
*
* @param object $platform_data Raw item.
* @return object Product object (with fieldData).
*/
private function extract_product( object $platform_data ): object {
if ( isset( $platform_data->product ) && is_object( $platform_data->product ) ) {
return $platform_data->product;
}
return $platform_data;
}
/**
* Returns the fieldData object on a product, or an empty stdClass for safety.
*
* @param object $product Product object.
* @return object
*/
private function extract_field_data( object $product ): object {
if ( isset( $product->fieldData ) && is_object( $product->fieldData ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
return $product->fieldData; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
}
return new \stdClass();
}
/**
* Returns the SKUs array off a wrapped item, or empty array.
*
* @param object $platform_data Raw item.
* @return array<int,object>
*/
private function extract_skus( object $platform_data ): array {
if ( isset( $platform_data->skus ) && is_array( $platform_data->skus ) ) {
return $platform_data->skus;
}
return array();
}
/**
* Returns the sku-properties array off fieldData, or empty array.
*
* @param object $field_data Field data.
* @return array<int,object>
*/
private function extract_sku_properties( object $field_data ): array {
$key = 'sku-properties';
if ( isset( $field_data->{$key} ) && is_array( $field_data->{$key} ) ) {
return $field_data->{$key};
}
return array();
}
/**
* Map fields common to every product (name, slug, description, status…).
*
* @param object $product Product object.
* @param object $field_data Field data object.
* @param bool $is_variable Whether this product has multiple SKUs to expose as variations.
* @return array
*/
private function map_basic_fields( object $product, object $field_data, bool $is_variable ): array {
$basic = array();
$basic['is_variable'] = $is_variable;
$basic['original_product_id'] = isset( $product->id ) ? (string) $product->id : null;
$basic['name'] = isset( $field_data->name ) ? sanitize_text_field( (string) $field_data->name ) : '';
$basic['slug'] = isset( $field_data->slug ) ? sanitize_title( (string) $field_data->slug ) : sanitize_title( $basic['name'] );
$basic['description'] = isset( $field_data->description ) ? wp_kses_post( (string) $field_data->description ) : '';
$short_description_key = 'short-description';
$basic['short_description'] = isset( $field_data->{$short_description_key} )
? wp_kses_post( (string) $field_data->{$short_description_key} )
: '';
// Status and visibility mirror the source on every import, so a re-run resets any manual
// draft/hide a merchant applied to a previously imported product. This is intentional: the
// migrator treats Webflow as the source of truth for these fields.
$basic['status'] = $this->map_status( $product );
$basic['catalog_visibility'] = 'visible';
if ( isset( $product->createdOn ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
$basic['date_created_gmt'] = (string) $product->createdOn; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
}
if ( isset( $product->lastUpdated ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
$basic['date_modified_gmt'] = (string) $product->lastUpdated; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
}
$basic['brand'] = null;
return $basic;
}
/**
* Map Webflow product publication flags to WC status.
*
* @param object $product Product object.
* @return string
*/
private function map_status( object $product ): string {
$is_archived = ! empty( $product->isArchived ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
$is_draft = ! empty( $product->isDraft ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
if ( $is_archived ) {
return 'draft';
}
if ( $is_draft ) {
return 'draft';
}
return 'publish';
}
/**
* Read the pre-resolved categories the fetcher attached to the item.
*
* @param object $platform_data Raw item.
* @return array<int,array{name:string,slug:string}>
*/
private function map_categories( object $platform_data ): array {
$resolved_key = '_resolved_categories';
if ( ! isset( $platform_data->{$resolved_key} ) || ! is_array( $platform_data->{$resolved_key} ) ) {
return array();
}
$categories = array();
foreach ( $platform_data->{$resolved_key} as $entry ) {
if ( is_array( $entry ) && ! empty( $entry['name'] ) ) {
$categories[] = array(
'name' => sanitize_text_field( (string) $entry['name'] ),
'slug' => sanitize_title( (string) ( $entry['slug'] ?? $entry['name'] ) ),
);
} elseif ( is_object( $entry ) && ! empty( $entry->name ) ) {
$categories[] = array(
'name' => sanitize_text_field( (string) $entry->name ),
'slug' => sanitize_title( (string) ( $entry->slug ?? $entry->name ) ),
);
}
}
return $categories;
}
/**
* Build the unified images array (product gallery + SKU images, deduped by URL).
*
* The importer requires every image referenced from a variation to also appear
* in this top-level array — that's how the original_id => attachment mapping is
* populated. We assign stable `original_id`s here (preferring Webflow's fileId,
* falling back to a URL hash) so SKU references resolve cleanly later.
*
* @param object $field_data Product fieldData.
* @param array<int,object> $skus SKUs array.
* @return array<int,array{original_id:string,src:string,alt:?string,is_featured:bool}>
*/
private function build_images( object $field_data, array $skus ): array {
$images = array();
$by_url = array();
$featured_url = null;
// Product-level gallery: fieldData['more-images'].
$more_images_key = 'more-images';
if ( isset( $field_data->{$more_images_key} ) && is_array( $field_data->{$more_images_key} ) ) {
foreach ( $field_data->{$more_images_key} as $img ) {
$entry = $this->normalize_image_object( $img );
if ( null === $entry ) {
continue;
}
if ( null === $featured_url ) {
$featured_url = $entry['src'];
}
$this->add_unique_image( $images, $by_url, $entry );
}
}
// SKU main-image + more-images.
$main_image_key = 'main-image';
foreach ( $skus as $sku ) {
$sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
if ( null === $sku_field ) {
continue;
}
$main_entry = isset( $sku_field->{$main_image_key} ) ? $this->normalize_image_object( $sku_field->{$main_image_key} ) : null;
if ( $main_entry ) {
if ( null === $featured_url ) {
$featured_url = $main_entry['src'];
}
$this->add_unique_image( $images, $by_url, $main_entry );
}
if ( isset( $sku_field->{$more_images_key} ) && is_array( $sku_field->{$more_images_key} ) ) {
foreach ( $sku_field->{$more_images_key} as $img ) {
$entry = $this->normalize_image_object( $img );
if ( $entry ) {
$this->add_unique_image( $images, $by_url, $entry );
}
}
}
}
// Mark featured.
foreach ( $images as &$image ) {
$image['is_featured'] = ( $image['src'] === $featured_url );
}
unset( $image );
return $images;
}
/**
* Normalize a Webflow image object into our images-array entry, or return null if unusable.
*
* @param mixed $img Raw Webflow image entry.
* @return array{original_id:string,src:string,alt:?string,is_featured:bool}|null
*/
private function normalize_image_object( $img ): ?array {
if ( ! is_object( $img ) ) {
return null;
}
$url = isset( $img->url ) ? (string) $img->url : '';
if ( '' === $url ) {
return null;
}
$alt = isset( $img->alt ) ? (string) $img->alt : null;
$original_id = '';
if ( isset( $img->fileId ) && '' !== (string) $img->fileId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
$original_id = (string) $img->fileId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
} elseif ( isset( $img->id ) && '' !== (string) $img->id ) {
$original_id = (string) $img->id;
} else {
$original_id = 'webflow-' . md5( $url );
}
return array(
'original_id' => $original_id,
'src' => $url,
'alt' => $alt,
'is_featured' => false,
);
}
/**
* Append an image entry to $images keyed by URL, deduping repeats.
*
* @param array $images Image list (mutated).
* @param array $by_url URL => index map (mutated).
* @param array $entry The image entry to add.
* @return void
*/
private function add_unique_image( array &$images, array &$by_url, array $entry ): void {
$url = $entry['src'];
if ( isset( $by_url[ $url ] ) ) {
return;
}
$by_url[ $url ] = count( $images );
$images[] = $entry;
}
/**
* Map fields specific to a simple (single-SKU) product.
*
* @param array<int,object> $skus SKUs.
* @return array
*/
private function map_simple_data( array $skus ): array {
$simple = array(
'sku' => null,
'regular_price' => null,
'sale_price' => null,
'manage_stock' => false,
'stock_quantity' => null,
'stock_status' => 'instock',
'tax_status' => 'taxable',
);
if ( empty( $skus ) ) {
return $simple;
}
$sku = $skus[0];
$sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
if ( null === $sku_field ) {
return $simple;
}
if ( $this->should_process( 'price' ) ) {
$prices = $this->extract_prices( $sku_field );
$simple['regular_price'] = $prices['regular_price'];
$simple['sale_price'] = $prices['sale_price'];
}
if ( $this->should_process( 'sku' ) && isset( $sku_field->sku ) ) {
$simple['sku'] = wc_clean( (string) $sku_field->sku );
}
if ( $this->should_process( 'stock' ) ) {
$stock = $this->extract_stock( $sku_field );
$simple['manage_stock'] = $stock['manage_stock'];
$simple['stock_quantity'] = $stock['stock_quantity'];
$simple['stock_status'] = $stock['stock_status'];
}
if ( $this->should_process( 'weight' ) ) {
$simple['weight'] = $this->extract_weight( $sku_field );
}
if ( $this->should_process( 'dimensions' ) ) {
$simple = array_merge( $simple, $this->extract_dimensions( $sku_field ) );
}
return $simple;
}
/**
* Map variable-product data: attribute definitions + per-variation rows.
*
* @param array<int,object> $properties sku-properties array.
* @param array<int,object> $skus SKUs array.
* @param array<int,array> $images Already-built images list (to resolve image references).
* @return array{attributes: array, variations: array}
*/
private function map_variable_data( array $properties, array $skus, array $images ): array {
$attributes = array();
// Build a lookup: property_id => [ 'name' => string, 'enums' => [ enum_id => option_name ] ].
$property_lookup = array();
$position = 0;
foreach ( $properties as $property ) {
if ( ! is_object( $property ) || empty( $property->id ) || empty( $property->name ) ) {
continue;
}
$enum_map = array();
$enum_options = array();
$enums = ( isset( $property->enum ) && is_array( $property->enum ) ) ? $property->enum : array();
foreach ( $enums as $enum ) {
if ( ! is_object( $enum ) || empty( $enum->id ) || empty( $enum->name ) ) {
continue;
}
$enum_map[ (string) $enum->id ] = (string) $enum->name;
$enum_options[] = (string) $enum->name;
}
$property_lookup[ (string) $property->id ] = array(
'name' => (string) $property->name,
'enums' => $enum_map,
);
if ( $this->should_process( 'attributes' ) ) {
$attributes[] = array(
'name' => wc_clean( (string) $property->name ),
'options' => array_map( 'wc_clean', $enum_options ),
'position' => $position,
'is_visible' => true,
'is_variation' => true,
);
}
++$position;
}
// Build URL => original_id map so we can resolve a SKU's main-image back to an entry in images[].
$url_to_original_id = array();
foreach ( $images as $image ) {
$url_to_original_id[ $image['src'] ] = $image['original_id'];
}
$variations = array();
$menu_order = 0;
foreach ( $skus as $sku ) {
$sku_field = isset( $sku->fieldData ) && is_object( $sku->fieldData ) ? $sku->fieldData : null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Webflow API uses camelCase.
if ( null === $sku_field ) {
continue;
}
$variation = array(
'original_id' => isset( $sku->id ) ? (string) $sku->id : null,
'sku' => null,
'regular_price' => null,
'sale_price' => null,
'manage_stock' => false,
'stock_quantity' => null,
'stock_status' => 'instock',
'tax_status' => 'taxable',
'attributes' => array(),
'image_original_id' => null,
'menu_order' => $menu_order,
);
if ( $this->should_process( 'sku' ) && isset( $sku_field->sku ) ) {
$variation['sku'] = wc_clean( (string) $sku_field->sku );
}
if ( $this->should_process( 'price' ) ) {
$prices = $this->extract_prices( $sku_field );
$variation['regular_price'] = $prices['regular_price'];
$variation['sale_price'] = $prices['sale_price'];
}
if ( $this->should_process( 'stock' ) ) {
$stock = $this->extract_stock( $sku_field );
$variation['manage_stock'] = $stock['manage_stock'];
$variation['stock_quantity'] = $stock['stock_quantity'];
$variation['stock_status'] = $stock['stock_status'];
}
if ( $this->should_process( 'weight' ) ) {
$variation['weight'] = $this->extract_weight( $sku_field );
}
if ( $this->should_process( 'dimensions' ) ) {
$variation = array_merge( $variation, $this->extract_dimensions( $sku_field ) );
}
if ( $this->should_process( 'attributes' ) ) {
$variation['attributes'] = $this->resolve_variation_attributes( $sku_field, $property_lookup );
if ( empty( $variation['attributes'] ) ) {
wc_get_logger()->debug(
sprintf(
'Webflow variation %s resolved to zero attributes; WooCommerce will store it as an "Any" variation, which can collide with sibling variations.',
$variation['original_id'] ?? 'unknown'
),
array( 'source' => 'wc-migrator' )
);
}
}
if ( $this->should_process( 'images' ) ) {
$main_image_key = 'main-image';
if ( isset( $sku_field->{$main_image_key}->url ) ) {
$url = (string) $sku_field->{$main_image_key}->url;
if ( isset( $url_to_original_id[ $url ] ) ) {
$variation['image_original_id'] = $url_to_original_id[ $url ];
}
}
}
$variations[] = $variation;
++$menu_order;
}
return array(
'attributes' => $attributes,
'variations' => $variations,
);
}
/**
* Resolve a SKU's `sku-values` (property_id => enum_id) into a human-readable
* `attribute_name => option_name` array using the property lookup.
*
* @param object $sku_field SKU fieldData.
* @param array $property_lookup Property lookup map.
* @return array<string,string>
*/
private function resolve_variation_attributes( object $sku_field, array $property_lookup ): array {
$resolved = array();
$values_key = 'sku-values';
if ( ! isset( $sku_field->{$values_key} ) ) {
return $resolved;
}
$values = $sku_field->{$values_key};
if ( ! is_object( $values ) && ! is_array( $values ) ) {
return $resolved;
}
foreach ( (array) $values as $property_id => $enum_id ) {
$property_id = (string) $property_id;
$enum_id = (string) $enum_id;
if ( ! isset( $property_lookup[ $property_id ] ) ) {
continue;
}
$property_name = $property_lookup[ $property_id ]['name'];
$option_name = $property_lookup[ $property_id ]['enums'][ $enum_id ] ?? null;
if ( null === $option_name ) {
continue;
}
$resolved[ sanitize_text_field( $property_name ) ] = sanitize_text_field( $option_name );
}
return $resolved;
}
/**
* Convert Webflow `price.value` (minor units, e.g. cents) into a decimal string,
* and detect sale pricing via `compare-at-price`.
*
* @param object $sku_field SKU fieldData.
* @return array{regular_price: ?string, sale_price: ?string}
*/
private function extract_prices( object $sku_field ): array {
$price = $this->price_to_decimal( $sku_field->price ?? null );
$compare_key = 'compare-at-price';
$compare_price = $this->price_to_decimal( $sku_field->{$compare_key} ?? null );
if ( null !== $compare_price && null !== $price && (float) $compare_price > (float) $price ) {
return array(
'regular_price' => $compare_price,
'sale_price' => $price,
);
}
return array(
'regular_price' => $price,
'sale_price' => null,
);
}
/**
* Convert a Webflow money object `{ value: int (minor units), unit: "USD" }` into a decimal string.
*
* The number of minor units per major unit varies by currency (e.g. JPY has 0,
* KWD has 3, most have 2), so the divisor is derived from the currency's
* `num_decimals` rather than a hardcoded `/ 100`.
*
* @param mixed $money Webflow money object.
* @return string|null
*/
private function price_to_decimal( $money ): ?string {
if ( ! is_object( $money ) || ! isset( $money->value ) ) {
return null;
}
$minor = (int) $money->value;
if ( $minor < 0 ) {
return null;
}
$unit = isset( $money->unit ) ? (string) $money->unit : 'USD';
$decimals = $this->get_currency_decimals( $unit );
return number_format( $minor / ( 10 ** $decimals ), $decimals, '.', '' );
}
/**
* Get the number of decimal places (minor units) for an ISO currency code.
*
* Reads core's `i18n/locale-info.php` so the migrator stays in sync with
* WooCommerce's own per-currency data. Falls back to 2 for unknown codes.
*
* @param string $currency ISO 4217 currency code.
* @return int
*/
private function get_currency_decimals( string $currency ): int {
if ( null === $this->currency_decimals ) {
$this->currency_decimals = array();
$locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';
if ( is_array( $locale_info ) ) {
foreach ( $locale_info as $info ) {
if ( isset( $info['currency_code'], $info['num_decimals'] ) ) {
$this->currency_decimals[ $info['currency_code'] ] = (int) $info['num_decimals'];
}
}
}
}
return $this->currency_decimals[ strtoupper( $currency ) ] ?? 2;
}
/**
* Map Webflow inventory shape to WC stock fields.
*
* @param object $sku_field SKU fieldData.
* @return array{manage_stock: bool, stock_quantity: ?int, stock_status: string}
*/
private function extract_stock( object $sku_field ): array {
$inventory = isset( $sku_field->inventory ) && is_object( $sku_field->inventory ) ? $sku_field->inventory : null;
if ( null === $inventory ) {
return array(
'manage_stock' => false,
'stock_quantity' => null,
'stock_status' => 'instock',
);
}
$type = isset( $inventory->type ) ? (string) $inventory->type : 'infinite';
if ( 'finite' !== $type ) {
return array(
'manage_stock' => false,
'stock_quantity' => null,
'stock_status' => 'instock',
);
}
$quantity = isset( $inventory->quantity ) ? (int) $inventory->quantity : 0;
return array(
'manage_stock' => true,
'stock_quantity' => $quantity,
'stock_status' => $quantity > 0 ? 'instock' : 'outofstock',
);
}
/**
* Extract Webflow SKU dimensions (length, width, height).
*
* Webflow returns these as raw numerics with no associated unit on the SKU
* payload — the unit is a store-level setting on the Webflow side, with no
* API representation. We pass through as-is; WooCommerce will interpret them
* in whatever `woocommerce_dimension_unit` is configured for the destination
* store. Null/zero/non-numeric values are dropped.
*
* @param object $sku_field SKU fieldData.
* @return array{length: ?float, width: ?float, height: ?float}
*/
private function extract_dimensions( object $sku_field ): array {
$dimensions = array(
'length' => null,
'width' => null,
'height' => null,
);
foreach ( array_keys( $dimensions ) as $key ) {
if ( isset( $sku_field->{$key} ) && is_numeric( $sku_field->{$key} ) && (float) $sku_field->{$key} > 0 ) {
$dimensions[ $key ] = (float) $sku_field->{$key};
}
}
return $dimensions;
}
/**
* Convert Webflow `weight` (with optional `weight-unit`) to the store's weight unit.
*
* @param object $sku_field SKU fieldData.
* @return float|null
*/
private function extract_weight( object $sku_field ): ?float {
if ( ! isset( $sku_field->weight ) ) {
return null;
}
$weight = (float) $sku_field->weight;
if ( $weight <= 0 ) {
return null;
}
$unit_key = 'weight-unit';
$source_unit = isset( $sku_field->{$unit_key} ) ? strtolower( (string) $sku_field->{$unit_key} ) : 'lbs';
$source_unit = $this->normalize_weight_unit( $source_unit );
$store_unit = strtolower( (string) get_option( 'woocommerce_weight_unit', 'kg' ) );
if ( $source_unit === $store_unit ) {
return $weight;
}
if ( function_exists( 'wc_get_weight' ) ) {
$converted = wc_get_weight( $weight, $store_unit, $source_unit );
return is_numeric( $converted ) ? (float) $converted : $weight;
}
return $weight;
}
/**
* Normalize a Webflow weight unit string to a wc_get_weight compatible unit.
*
* @param string $unit Raw unit string.
* @return string
*/
private function normalize_weight_unit( string $unit ): string {
$map = array(
'oz' => 'oz',
'lb' => 'lbs',
'lbs' => 'lbs',
'pound' => 'lbs',
'g' => 'g',
'gram' => 'g',
'kg' => 'kg',
);
return $map[ $unit ] ?? 'lbs';
}
/**
* Map Webflow SEO fields (`seo-title`, `seo-description`) into a metafields array.
*
* @param object $field_data Field data.
* @return array<string,string>
*/
private function map_seo( object $field_data ): array {
$meta = array();
$title_key = 'seo-title';
$desc_key = 'seo-description';
if ( isset( $field_data->{$title_key} ) && '' !== (string) $field_data->{$title_key} ) {
$meta['global_title_tag'] = (string) $field_data->{$title_key};
}
if ( isset( $field_data->{$desc_key} ) && '' !== (string) $field_data->{$desc_key} ) {
$meta['global_description_tag'] = (string) $field_data->{$desc_key};
}
return $meta;
}
/**
* Should this field be processed?
*
* @param string $field_key The field key.
* @return bool
*/
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 );
}
}