Files
2026-04-28 15:13:50 +02:00

872 lines
29 KiB
PHP

<?php
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
namespace WooCommerce\Facebook;
defined( 'ABSPATH' ) || exit;
use WC_Facebook_Product;
use WC_Product;
use WooCommerce\Facebook\Products;
/**
* Product Attribute Mapper for WooCommerce to Meta.
*
* This class provides a comprehensive mapping system for WooCommerce product attributes
* to Meta catalog fields, enhancing the default mapping with more flexibility
* and better support for custom attributes.
*
* @since 3.5.4
*/
class ProductAttributeMapper {
/** @var array Standard Facebook fields that WooCommerce attributes can map to */
private static $standard_facebook_fields = array(
'size' => array( 'size' ),
'color' => array( 'color', 'colour' ),
'pattern' => array( 'pattern' ),
'material' => array( 'material' ),
'gender' => array( 'gender' ),
'age_group' => array( 'age_group' ),
'brand' => array( 'brand', 'manufacturer' ),
'condition' => array( 'condition', 'state' ),
'mpn' => array( 'mpn', 'manufacturer_part_number' ),
'gtin' => array( 'gtin', 'upc', 'ean', 'jan', 'isbn' ),
'google_product_category' => array( 'google_product_category', 'product_category', 'category' ),
);
/** @var array Extended Facebook fields based on Meta commerce platform catalog fields */
private static $extended_facebook_fields = array(
'sale_price' => array( 'sale_price', 'discount_price', 'offer_price' ),
'inventory' => array( 'inventory', 'stock', 'quantity' ),
);
/** @var array Maps WooCommerce attribute naming variations to standardized Meta field names */
private static $attribute_name_mapping = array(
// Common naming variations for color
'product_color' => 'color',
'item_color' => 'color',
'color_family' => 'color',
// Common naming variations for size
'product_size' => 'size',
'item_size' => 'size',
'shoe_size' => 'size',
'clothing_size' => 'size',
// Common naming variations for gender
'target_gender' => 'gender',
'product_gender' => 'gender',
// Common naming variations for material
'product_material' => 'material',
'fabric' => 'material',
'item_material' => 'material',
// Common naming variations for pattern
'product_pattern' => 'pattern',
'design' => 'pattern',
// Common naming variations for age group
'product_age_group' => 'age_group',
'target_age' => 'age_group',
'age_range' => 'age_group',
// Common naming variations for brand
'product_brand' => 'brand',
'manufacturer_name' => 'brand',
// Common naming variations for condition
'product_condition' => 'condition',
'item_condition' => 'condition',
);
/** @var bool Flag to track if custom mappings have been loaded */
private static $custom_mappings_loaded = false;
/**
* Initializes the attribute mappings by loading custom mappings from the database.
* This ensures that mappings created through the UI are available for use.
*
* @since 3.0.32
*
* @return void
*/
private static function load_custom_mappings() {
if ( self::$custom_mappings_loaded ) {
return;
}
// Load custom mappings from the database
// NOTE: The option name wc_facebook_custom_attribute_mappings is defined in Admin/Settings_Screens/Product_Attributes.php
$custom_mappings = get_option( 'wc_facebook_custom_attribute_mappings', array() );
if ( ! empty( $custom_mappings ) && is_array( $custom_mappings ) ) {
foreach ( $custom_mappings as $wc_attribute => $fb_field ) {
$sanitized_attribute = self::sanitize_attribute_name( $wc_attribute );
self::$attribute_name_mapping[ $sanitized_attribute ] = $fb_field;
}
}
self::$custom_mappings_loaded = true;
}
/**
* Gets all standardized Meta catalog fields.
*
* @since 3.5.4
*
* @return array Array of all supported Meta fields with their variations
*/
public static function get_all_facebook_fields() {
return array_merge( self::$standard_facebook_fields, self::$extended_facebook_fields );
}
/**
* Check if a WooCommerce attribute maps to a standard Facebook field
*
* @since 3.5.4
*
* @param string $attribute_name The WooCommerce attribute name
* @return bool|string False if not mapped, or the Facebook field name if mapped
*/
public static function check_attribute_mapping( $attribute_name ) {
// Ensure custom mappings are loaded
self::load_custom_mappings();
// Clean the attribute name
$sanitized_name = self::sanitize_attribute_name( $attribute_name );
// Check if there's a direct mapping in our attribute_name_mapping
if ( isset( self::$attribute_name_mapping[ $sanitized_name ] ) ) {
$result = self::$attribute_name_mapping[ $sanitized_name ];
return $result;
}
// Check for exact matches in standard fields
foreach ( self::$standard_facebook_fields as $fb_field => $possible_matches ) {
if ( in_array( $sanitized_name, $possible_matches, true ) ) {
return $fb_field;
}
}
// If no exact match in standard fields, check if the attribute is a standard field itself
if ( isset( self::$standard_facebook_fields[ $sanitized_name ] ) ) {
return $sanitized_name;
}
// DISABLED: Fuzzy matching can lead to unpredictable attribute mappings
// We now only use explicit mappings (from UI) and exact matches for consistent behavior
// This ensures that attributes are only mapped when the store owner explicitly intends them to be
return false;
}
/**
* Sanitizes an attribute name for use in custom data fields
*
* @since 2.6.0
*
* @param string $attribute_name The raw attribute name
* @return string
*/
public static function sanitize_attribute_name( $attribute_name ) {
// First, get the original attribute name without the pa_ prefix
$original_name = preg_replace( '/^pa_/', '', $attribute_name );
// If the attribute is a taxonomy attribute and likely has a display name,
// try to get the display name first (e.g., "Material" instead of "pa_material")
if ( strpos( $attribute_name, 'pa_' ) === 0 ) {
$taxonomy = get_taxonomy( $attribute_name );
if ( $taxonomy && ! empty( $taxonomy->labels->singular_name ) ) {
// Return the display name in lowercase for standardization
return strtolower( $taxonomy->labels->singular_name );
}
}
// Remove pa_ prefix from taxonomy attributes
$attribute_name = $original_name;
// Convert spaces and special characters to underscores
$attribute_name = strtolower( $attribute_name );
$attribute_name = preg_replace( '/[^a-z0-9_]/', '_', $attribute_name );
$attribute_name = preg_replace( '/_+/', '_', $attribute_name );
$attribute_name = trim( $attribute_name, '_' );
return $attribute_name;
}
/**
* Get all attributes that are not mapped to standard Facebook fields
*
* @since 3.5.4
*
* @param WC_Product $product The WooCommerce product
* @return array Array of unmapped attributes with 'name' and 'value' keys
*/
public static function get_unmapped_attributes( WC_Product $product ) {
// Ensure custom mappings are loaded first
self::load_custom_mappings();
$unmapped_attributes = array();
$attributes = $product->get_attributes();
foreach ( $attributes as $attribute_name => $_ ) {
$value = $product->get_attribute( $attribute_name );
if ( ! empty( $value ) ) {
// Use the comprehensive check_attribute_mapping method to determine if mapped
$mapped_field = self::check_attribute_mapping( $attribute_name );
// If no mapping found, it's unmapped
if ( false === $mapped_field ) {
$unmapped_attributes[] = array(
'name' => $attribute_name,
'value' => $value,
);
}
}
}
return $unmapped_attributes;
}
/**
* Gets all mapped attributes for a product.
*
* @since 3.5.4
*
* @param WC_Product $product The WooCommerce product
* @return array Array of mapped attributes with Meta field name as key and attribute value as value
*/
public static function get_mapped_attributes( WC_Product $product ) {
// Ensure custom mappings are loaded
self::load_custom_mappings();
$mapped_attributes = array();
$attributes = $product->get_attributes();
// Get manual attribute mappings from the plugin settings
$custom_mappings = get_option( 'wc_facebook_custom_attribute_mappings', array() );
// Filters the product attribute mappings.
/**
* Filters the product attribute mappings.
*
* @since 3.5.4
*
* @param array $mappings The attribute mappings
* @param WC_Product $product The product object
*/
$filtered_mappings = apply_filters( 'wc_facebook_product_attribute_mappings', array(), $product );
// Create a map to track exact slug matches first
$slug_to_fb_field = array(
'brand' => 'brand',
'age-group' => 'age_group',
'age_group' => 'age_group',
'agegroup' => 'age_group',
'gender' => 'gender',
'material' => 'material',
'condition' => 'condition',
'color' => 'color',
'colour' => 'color',
'size' => 'size',
'pattern' => 'pattern',
'mpn' => 'mpn',
);
// Store attributes that have already been processed to avoid duplicates
$processed_fb_fields = array();
// Get a complete list of standard Facebook fields for prioritization
$standard_fields = array( 'brand', 'color', 'size', 'pattern', 'material', 'gender', 'age_group', 'condition', 'mpn' );
// Create a priority map to resolve conflicts - higher number = higher priority
$priority_map = array(
'direct_match' => 100, // Highest priority - direct match by attribute name
'slug_match' => 80, // High priority - slug match
'mapped' => 60, // Medium priority - mapped via check_attribute_mapping
'custom_mapped' => 50, // Medium priority - manually mapped via UI (reduced from 90)
'meta' => 20, // Low-medium priority - meta value
);
// Track attribute sources and their priorities for later conflict resolution
$attribute_sources = array();
// PHASE 0: First process any custom attribute mappings set via the UI
foreach ( $filtered_mappings as $wc_attr_name => $fb_field ) {
// Find the attribute in the product
foreach ( $attributes as $attribute_key => $attribute ) {
$clean_key = self::sanitize_attribute_name( $attribute_key );
$clean_name = self::sanitize_attribute_name( $wc_attr_name );
if ( $clean_key === $clean_name ) {
$value = $product->get_attribute( $attribute_key );
if ( ! empty( $value ) ) {
// Save the attribute with its priority
if ( ! isset( $attribute_sources[ $fb_field ] ) || $priority_map['custom_mapped'] > $attribute_sources[ $fb_field ]['priority'] ) {
$attribute_sources[ $fb_field ] = array(
'value' => $value,
'priority' => $priority_map['custom_mapped'],
'source' => "UI mapping: {$wc_attr_name}",
);
}
}
}
}
}
// PHASE 1: Now check for direct standard field matches by attribute name
foreach ( $attributes as $attribute_name => $attribute ) {
$value = $product->get_attribute( $attribute_name );
if ( ! empty( $value ) ) {
if ( is_object( $attribute ) && method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy() && strpos( $value, ', ' ) !== false ) {
$value = str_replace( ', ', ' | ', $value );
}
// Clean up attribute name for matching
$clean_name = self::sanitize_attribute_name( $attribute_name );
// Direct match with standard fields - fix: also check attribute display name
foreach ( self::$standard_facebook_fields as $fb_field => $possible_matches ) {
// Get the attribute's display name by removing "pa_" prefix and converting underscores to spaces
$display_name = ucfirst( str_replace( '_', ' ', preg_replace( '/^pa_/', '', $attribute_name ) ) );
$lower_display_name = strtolower( $display_name );
// Also check display name with hyphens converted to underscores (for attributes like "age-group")
$display_name_with_underscores = str_replace( '-', '_', $lower_display_name );
// If the attribute name exactly matches the standard field name OR
// the display name matches the field name (case insensitive) OR
// the display name with hyphens converted to underscores matches
if ( $clean_name === $fb_field || $lower_display_name === $fb_field || $display_name_with_underscores === $fb_field ) {
// Save the attribute with its priority
if ( ! isset( $attribute_sources[ $fb_field ] ) || $priority_map['direct_match'] > $attribute_sources[ $fb_field ]['priority'] ) {
$attribute_sources[ $fb_field ] = array(
'value' => $value,
'priority' => $priority_map['direct_match'],
'source' => "Direct match: {$attribute_name}",
);
}
break;
}
}
}
}
// PHASE 2: Look for exact slug matches
foreach ( $attributes as $attribute_name => $attribute ) {
$value = $product->get_attribute( $attribute_name );
if ( ! empty( $value ) ) {
// Fix delimiter issue: WooCommerce uses commas for global attributes, but we need pipes
if ( is_object( $attribute ) && method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy() && strpos( $value, ', ' ) !== false ) {
$value = str_replace( ', ', ' | ', $value );
}
// Get both the original slug (with hyphens) and sanitized name (with underscores)
$original_slug = preg_replace( '/^pa_/', '', $attribute_name ); // Remove pa_ but keep hyphens
$clean_name = self::sanitize_attribute_name( $attribute_name ); // This converts hyphens to underscores
// Check for exact match in our slug mapping using both forms
$fb_field = null;
if ( isset( $slug_to_fb_field[ $original_slug ] ) ) {
$fb_field = $slug_to_fb_field[ $original_slug ];
} elseif ( isset( $slug_to_fb_field[ $clean_name ] ) ) {
$fb_field = $slug_to_fb_field[ $clean_name ];
}
if ( $fb_field ) {
// Save the attribute with its priority
if ( ! isset( $attribute_sources[ $fb_field ] ) || $priority_map['slug_match'] > $attribute_sources[ $fb_field ]['priority'] ) {
$attribute_sources[ $fb_field ] = array(
'value' => $value,
'priority' => $priority_map['slug_match'],
'source' => "Slug match: {$attribute_name}",
);
}
}
}
}
// PHASE 3: Look for mapped attributes via check_attribute_mapping
foreach ( $attributes as $attribute_name => $attribute ) {
$value = $product->get_attribute( $attribute_name );
if ( ! empty( $value ) && ! empty( $attribute_name ) ) {
// Fix delimiter issue: WooCommerce uses commas for global attributes, but we need pipes
if ( is_object( $attribute ) && method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy() && strpos( $value, ', ' ) !== false ) {
$value = str_replace( ', ', ' | ', $value );
}
$mapped_field = self::check_attribute_mapping( $attribute_name );
// Skip if no mapping found
if ( false !== $mapped_field ) {
// Normalize certain field values to conform to Facebook requirements
$original_value = $value;
switch ( $mapped_field ) {
case 'gender':
$value = self::normalize_gender_value( $value );
break;
case 'age_group':
$value = self::normalize_age_group_value( $value );
break;
case 'condition':
$value = self::normalize_condition_value( $value );
break;
}
// Save the attribute with its priority
if ( ! isset( $attribute_sources[ $mapped_field ] ) || $priority_map['mapped'] > $attribute_sources[ $mapped_field ]['priority'] ) {
$attribute_sources[ $mapped_field ] = array(
'value' => $value,
'priority' => $priority_map['mapped'],
'source' => "Mapped via check_attribute_mapping: {$attribute_name}",
);
}
}
}
}
// PHASE 4: For fields not found in product attributes, check meta values
foreach ( $standard_fields as $field ) {
// Check meta values only if we haven't found a higher priority source
if ( ! isset( $attribute_sources[ $field ] ) || $attribute_sources[ $field ]['priority'] < $priority_map['meta'] ) {
// Check for alternative storage in dedicated meta fields
$meta_key = '_wc_facebook_enhanced_catalog_attributes_' . $field;
$meta_value = $product->get_meta( $meta_key, true );
if ( ! empty( $meta_value ) ) {
$attribute_sources[ $field ] = array(
'value' => $meta_value,
'priority' => $priority_map['meta'],
'source' => 'Meta value',
);
}
}
}
// Now build the final mapped attributes based on priority
foreach ( $attribute_sources as $fb_field => $source_data ) {
$mapped_attributes[ $fb_field ] = $source_data['value'];
}
return $mapped_attributes;
}
/**
* Normalizes gender values to Facebook's expected format.
*
* @since 3.5.4
*
* @param string $value The original gender value
* @return string Normalized gender value
*/
public static function normalize_gender_value( $value ) {
$value = strtolower( trim( $value ) );
// Map common gender values to Facebook's expected values
$gender_map = array(
'men' => WC_Facebook_Product::GENDER_MALE,
'man' => WC_Facebook_Product::GENDER_MALE,
'boy' => WC_Facebook_Product::GENDER_MALE,
'boys' => WC_Facebook_Product::GENDER_MALE,
'masculine' => WC_Facebook_Product::GENDER_MALE,
'women' => WC_Facebook_Product::GENDER_FEMALE,
'woman' => WC_Facebook_Product::GENDER_FEMALE,
'girl' => WC_Facebook_Product::GENDER_FEMALE,
'girls' => WC_Facebook_Product::GENDER_FEMALE,
'feminine' => WC_Facebook_Product::GENDER_FEMALE,
'unisex' => WC_Facebook_Product::GENDER_UNISEX,
'uni sex' => WC_Facebook_Product::GENDER_UNISEX,
'uni-sex' => WC_Facebook_Product::GENDER_UNISEX,
'neutral' => WC_Facebook_Product::GENDER_UNISEX,
'all' => WC_Facebook_Product::GENDER_UNISEX,
);
return isset( $gender_map[ $value ] ) ? $gender_map[ $value ] : $value;
}
/**
* Normalizes age group values to Facebook's expected format.
*
* @since 3.5.4
*
* @param string $value The original age group value
* @return string Normalized age group value
*/
public static function normalize_age_group_value( $value ) {
$value = strtolower( trim( $value ) );
// Map common age group values to Facebook's expected values
$age_group_map = array(
'adult' => WC_Facebook_Product::AGE_GROUP_ADULT,
'adults' => WC_Facebook_Product::AGE_GROUP_ADULT,
'grown-up' => WC_Facebook_Product::AGE_GROUP_ADULT,
'grownup' => WC_Facebook_Product::AGE_GROUP_ADULT,
'all ages' => WC_Facebook_Product::AGE_GROUP_ALL_AGES,
'everyone' => WC_Facebook_Product::AGE_GROUP_ALL_AGES,
'any' => WC_Facebook_Product::AGE_GROUP_ALL_AGES,
'teen' => WC_Facebook_Product::AGE_GROUP_TEEN,
'teens' => WC_Facebook_Product::AGE_GROUP_TEEN,
'teenager' => WC_Facebook_Product::AGE_GROUP_TEEN,
'teenagers' => WC_Facebook_Product::AGE_GROUP_TEEN,
'adolescent' => WC_Facebook_Product::AGE_GROUP_TEEN,
'kid' => WC_Facebook_Product::AGE_GROUP_KIDS,
'kids' => WC_Facebook_Product::AGE_GROUP_KIDS,
'child' => WC_Facebook_Product::AGE_GROUP_KIDS,
'children' => WC_Facebook_Product::AGE_GROUP_KIDS,
'toddler' => WC_Facebook_Product::AGE_GROUP_TODDLER,
'toddlers' => WC_Facebook_Product::AGE_GROUP_TODDLER,
'infant' => WC_Facebook_Product::AGE_GROUP_INFANT,
'infants' => WC_Facebook_Product::AGE_GROUP_INFANT,
'baby' => WC_Facebook_Product::AGE_GROUP_INFANT,
'babies' => WC_Facebook_Product::AGE_GROUP_INFANT,
'newborn' => WC_Facebook_Product::AGE_GROUP_NEWBORN,
'newborns' => WC_Facebook_Product::AGE_GROUP_NEWBORN,
);
return isset( $age_group_map[ $value ] ) ? $age_group_map[ $value ] : $value;
}
/**
* Normalizes condition values to Facebook's expected format.
*
* @since 3.5.4
*
* @param string $value The original condition value
* @return string Normalized condition value
*/
private static function normalize_condition_value( $value ) {
$value = strtolower( trim( $value ) );
// Map common condition values to Facebook's expected values
$condition_map = array(
'new' => WC_Facebook_Product::CONDITION_NEW,
'brand new' => WC_Facebook_Product::CONDITION_NEW,
'brand-new' => WC_Facebook_Product::CONDITION_NEW,
'newest' => WC_Facebook_Product::CONDITION_NEW,
'sealed' => WC_Facebook_Product::CONDITION_NEW,
'used' => WC_Facebook_Product::CONDITION_USED,
'pre-owned' => WC_Facebook_Product::CONDITION_USED,
'preowned' => WC_Facebook_Product::CONDITION_USED,
'pre owned' => WC_Facebook_Product::CONDITION_USED,
'second hand' => WC_Facebook_Product::CONDITION_USED,
'secondhand' => WC_Facebook_Product::CONDITION_USED,
'second-hand' => WC_Facebook_Product::CONDITION_USED,
'refurbished' => WC_Facebook_Product::CONDITION_REFURBISHED,
'renewed' => WC_Facebook_Product::CONDITION_REFURBISHED,
'refreshed' => WC_Facebook_Product::CONDITION_REFURBISHED,
'reconditioned' => WC_Facebook_Product::CONDITION_REFURBISHED,
);
return isset( $condition_map[ $value ] ) ? $condition_map[ $value ] : $value;
}
/**
* Adds a custom mapping from a WooCommerce attribute to a Facebook field.
*
* @since 3.5.4
*
* @param string $wc_attribute The WooCommerce attribute name
* @param string $fb_field The Facebook field to map to
* @return bool Whether the mapping was added successfully
*/
public static function add_custom_attribute_mapping( $wc_attribute, $fb_field ) {
$sanitized_attribute = self::sanitize_attribute_name( $wc_attribute );
// Make sure the Facebook field is valid
$all_fields = array_keys( self::get_all_facebook_fields() );
if ( ! in_array( $fb_field, $all_fields, true ) ) {
return false;
}
// Add the mapping
self::$attribute_name_mapping[ $sanitized_attribute ] = $fb_field;
return true;
}
/**
* Removes a custom attribute mapping.
*
* @since 3.5.4
*
* @param string $wc_attribute The WooCommerce attribute name
* @return bool Whether the mapping was removed successfully
*/
public static function remove_custom_attribute_mapping( $wc_attribute ) {
$sanitized_attribute = self::sanitize_attribute_name( $wc_attribute );
if ( isset( self::$attribute_name_mapping[ $sanitized_attribute ] ) ) {
unset( self::$attribute_name_mapping[ $sanitized_attribute ] );
return true;
}
return false;
}
/**
* Sets all custom mappings from an associative array.
*
* @since 3.5.4
*
* @param array $mappings Associative array of WooCommerce attribute => Facebook field
* @return int Number of successfully added mappings
*/
public static function set_custom_attribute_mappings( array $mappings ) {
$success_count = 0;
foreach ( $mappings as $wc_attribute => $fb_field ) {
if ( self::add_custom_attribute_mapping( $wc_attribute, $fb_field ) ) {
++$success_count;
}
}
// Mark custom mappings as loaded
self::$custom_mappings_loaded = true;
return $success_count;
}
/**
* Gets all currently defined custom attribute mappings.
*
* @since 3.5.4
*
* @return array Associative array of custom attribute mappings
*/
public static function get_custom_attribute_mappings() {
return self::$attribute_name_mapping;
}
/**
* Prepares a product's attributes for Facebook according to the mapping.
*
* @since 3.5.4
*
* @param WC_Product $product The WooCommerce product
* @return array Array of Facebook-mapped attributes ready for the API
*/
public static function prepare_product_attributes_for_facebook( WC_Product $product ) {
$mapped_attributes = self::get_mapped_attributes( $product );
$fb_ready_attributes = array();
// Process each mapped attribute according to Facebook's requirements
foreach ( $mapped_attributes as $fb_field => $value ) {
switch ( $fb_field ) {
case 'gender':
case 'age_group':
case 'condition':
// These fields are already normalized
$fb_ready_attributes[ $fb_field ] = $value;
break;
case 'color':
case 'size':
case 'pattern':
case 'material':
case 'brand':
case 'mpn':
// These fields should be trimmed and limited
$fb_ready_attributes[ $fb_field ] = substr( trim( $value ), 0, 100 );
break;
default:
// For all other fields, just pass the value
$fb_ready_attributes[ $fb_field ] = $value;
break;
}
}
return $fb_ready_attributes;
}
/**
* Gets mapped attributes that correspond to the standard Facebook product fields.
*
* @since 2.6.0
*
* @param \WC_Product $product the product object
* @return array
*/
public static function get_mapped_standard_attributes( \WC_Product $product ) {
$all_mapped_attributes = self::get_mapped_attributes( $product );
$standard_field_names = array(
'brand',
'condition',
'gender',
'color',
'size',
'pattern',
'material',
'age_group',
);
$standard_attributes = array();
foreach ( $standard_field_names as $field ) {
if ( isset( $all_mapped_attributes[ $field ] ) ) {
$standard_attributes[ $field ] = $all_mapped_attributes[ $field ];
}
}
return $standard_attributes;
}
/**
* Saves mapped attributes to product meta.
*
* @since 3.5.4
*
* @param WC_Product $product The WooCommerce product
* @param array $mapped_attributes Array of mapped attributes to save (optional, if not provided will map attributes first)
* @return array The mapped attributes that were saved
*/
public static function save_mapped_attributes( WC_Product $product, $mapped_attributes = null ) {
if ( null === $mapped_attributes ) {
$mapped_attributes = self::get_mapped_attributes( $product );
}
$product_id = $product->get_id();
// Save each mapped attribute to product meta using the correct meta keys
foreach ( $mapped_attributes as $field_name => $value ) {
switch ( $field_name ) {
// Standard Facebook fields - these use fb_ prefix
case 'brand':
$meta_key = 'fb_brand';
break;
case 'color':
$meta_key = 'fb_color';
break;
case 'material':
$meta_key = 'fb_material';
break;
case 'size':
$meta_key = 'fb_size';
break;
case 'pattern':
$meta_key = 'fb_pattern';
break;
case 'age_group':
$meta_key = 'fb_age_group';
break;
case 'gender':
$meta_key = 'fb_gender';
break;
case 'condition':
$meta_key = 'fb_product_condition';
break;
case 'mpn':
$meta_key = 'fb_mpn';
break;
case 'gtin':
$meta_key = 'fb_gtin';
break;
// Extended Facebook fields - these use different patterns
case 'sale_price':
$meta_key = '_wc_facebook_sale_price';
break;
case 'inventory':
$meta_key = '_wc_facebook_inventory';
break;
case 'shipping_weight':
$meta_key = '_wc_facebook_shipping_weight';
break;
case 'shipping':
$meta_key = '_wc_facebook_shipping';
break;
case 'tax':
$meta_key = '_wc_facebook_tax';
break;
case 'image_link':
$meta_key = '_wc_facebook_image_link';
break;
case 'additional_image_link':
$meta_key = '_wc_facebook_additional_image_link';
break;
// For any other extended fields or unknown fields
default:
// Use enhanced catalog attributes pattern for other fields
$meta_key = '_wc_facebook_enhanced_catalog_attributes_' . $field_name;
break;
}
// Update the meta value
update_post_meta( $product_id, $meta_key, $value );
}
// Clear WordPress meta cache to ensure fresh values are read
wp_cache_delete( $product_id, 'post_meta' );
// Also clear WooCommerce product cache
if ( function_exists( 'wc_delete_product_transients' ) ) {
wc_delete_product_transients( $product_id );
}
// Clear any object cache for this product
clean_post_cache( $product_id );
return $mapped_attributes;
}
/**
* Gets mapped attributes and saves them to product meta in one operation.
*
* @since 3.5.4
*
* @param WC_Product $product The WooCommerce product
* @return array The mapped attributes that were saved
*/
public static function get_and_save_mapped_attributes( WC_Product $product ) {
try {
$mapped_attributes = self::get_mapped_attributes( $product );
$result = self::save_mapped_attributes( $product, $mapped_attributes );
return $result;
} catch ( \Exception $e ) {
error_log( 'ProductAttributeMapper sync error: ' . $e->getMessage() );
return array();
}
}
/**
* Initialize the attribute mapper.
*
* @since 3.5.4
*
* @return void
*/
public static function init() {
// Load custom mappings when the class is initialized
self::load_custom_mappings();
}
}
// Initialize when WooCommerce is fully loaded
add_action( 'woocommerce_init', array( 'WooCommerce\Facebook\ProductAttributeMapper', 'init' ), 10 );
// Also try initializing on plugins_loaded as a fallback
add_action( 'plugins_loaded', array( 'WooCommerce\Facebook\ProductAttributeMapper', 'init' ), 20 );