WooCommerce Composite Products
WooCommerce Composite Products lets customers build configurable product sets from selectable components. Each composite product generates a parent cart item plus child component items, all linked by composite_data, composite_parent, and composite_children meta keys. Phone Orders for WooCommerce doesn’t recognise the composite product type by default, and its cart update logic doesn’t carry composite relationship meta through rebuilds — breaking the parent-child structure and causing incorrect pricing when agents load or edit composite orders.
This snippet registers the composite product type, preserves all composite relationship data through cart updates, locks prices on both parent and child items, and prevents Phone Orders from double-adding component items that composites manage internally.
Code Sample
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
add_filter('wpo_search_product_types', function ($types) { $types[] = 'composite'; return $types; }); add_filter('wpo_skip_add_to_cart_item', function ($skip, $item) { return isset($item['composite_parent']) && $item['composite_parent'] ? true : $skip; }, 10, 2); add_filter('wpo_is_child_cart_item', function ($is_child, $item) { return isset($item['composite_parent']) && $item['composite_parent'] ? true : $is_child; }, 10, 2); add_filter('wpo_children_cart_item', function ($children, $item) { return isset($item['composite_children']) ? $item['composite_children'] : $children; }, 10, 2); add_filter('wpo_cart_item_is_price_readonly', function ($is_readonly, $cart_item_data) { return isset($cart_item_data['composite_parent']) && $cart_item_data['composite_parent'] || isset($cart_item_data['composite_children']) && $cart_item_data['composite_children'] ? true : $is_readonly; }, 10, 2); add_filter('wpo_update_cart_cart_item_meta', function ($cart_item_meta, $item) { return array_merge($cart_item_meta, array( 'composite_data' => isset($item['composite_data']) ? $item['composite_data'] : null, 'readonly_price' => isset($item['readonly_price']) ? $item['readonly_price'] : null, )); }, 10, 2); add_filter('wpo_update_cart_loaded_product', function ($loaded_product, $item) { return array_merge($loaded_product, array( 'composite_data' => isset($item['composite_data']) ? $item['composite_data'] : null, 'readonly_price' => isset($item['readonly_price']) ? $item['readonly_price'] : null, )); }, 10, 2); add_filter( 'wpo_get_item_by_product', function ( $product, $cart_item_data ) { $composite_data = isset( $cart_item_data['composite_data'] ) ? $cart_item_data['composite_data'] : null; $readonly_price = isset( $cart_item_data['readonly_price'] ) ? $cart_item_data['readonly_price'] : null; if ( ! $composite_data && isset( $cart_item_data['meta_data'] ) && is_array( $cart_item_data['meta_data'] ) ) { foreach ( $cart_item_data['meta_data'] as $index => $meta ) { $d = $meta->get_data(); if ( '_composite_data' === $d['key'] ) { $composite_data = $d['value']; break; } } } foreach ( $product['custom_meta_fields'] as $index => $meta_field ) { if ( '_composite_data' === $meta_field['meta_key'] ) { unset( $product['custom_meta_fields'][ $index ] ); break; } } return array_merge( $product, array( 'composite_data' => $composite_data, 'readonly_price' => $readonly_price, ) ); }, 10, 2 ); add_filter('wpo_add_configured_products_skip_item', function ($skip, $cart_item_data) { return isset($cart_item_data['composite_parent']) ? true : $skip; }, 10, 2); add_filter('wpo_load_order_skip_item', function ($skip, $order_item, $order) { $item_data = $order_item->get_data(); if (isset($item_data['meta_data']) && is_array($item_data['meta_data'])) { foreach ($item_data['meta_data'] as $meta) { $d = $meta->get_data(); if ( '_composite_parent' === $d['key'] ) { return true; } } } return $skip; }, 10, 3); add_filter('wpo_add_configured_products_item_data', function ($item_data, $cart_item_data, $cart_item_key, $wc_product) { if ( isset( $cart_item_data['composite_children'] ) ) { $readonly_price = apply_filters( 'woocommerce_cart_item_price', WC()->cart->get_product_price( $wc_product ), $cart_item_data, $cart_item_key ); $available_symbols = implode("|", array('\' . wc_get_price_decimal_separator(), '\' . wc_get_price_thousand_separator())); $pattern = "/</span>([d|{$available_symbols}]+)?/i"; if (preg_match($pattern, $readonly_price, $matches)) { $item_data['readonly_price'] = $matches[1]; } } return $item_data; }, 10, 4); |
Code Explained (for Developers)
The snippet covers the full lifecycle of a composite product inside Phone Orders — from search, through cart building, to order loading.
| Hook | Purpose |
|---|---|
wpo_search_product_types | Registers 'composite' so composite products appear in Phone Orders product search. |
wpo_skip_add_to_cart_item | Prevents Phone Orders from re-adding component child items independently. WooCommerce Composite Products adds children automatically when the parent is added — Phone Orders must not add them a second time. |
wpo_is_child_cart_item | Marks items with a composite_parent key as child items in Phone Orders’ cart structure, so they nest correctly under their parent in the UI. |
wpo_children_cart_item | Passes the composite_children array to Phone Orders so it can correctly associate child item cart keys with their parent. |
wpo_cart_item_is_price_readonly | Locks the price field for both parent items (which have composite_children) and child items (which have composite_parent). Composite pricing is calculated by WooCommerce Composite Products from component selections — manual agent edits would break this. |
wpo_update_cart_cart_item_meta and wpo_update_cart_loaded_product | Persist composite_data and readonly_price through every cart update and reload cycle, preventing composite configuration data from being lost when Phone Orders rebuilds the cart. |
wpo_get_item_by_product | Passes composite_data to the Phone Orders item object on resolution, with a fallback that reads _composite_data from order item meta when loading existing orders. Also removes _composite_data from custom_meta_fields to prevent it appearing as a raw meta display in the UI. |
wpo_add_configured_products_skip_item | Skips child items when Phone Orders reconstructs a configured product set, since the composite parent’s add-to-cart handles children. |
wpo_load_order_skip_item | Skips composite child items when loading an existing order into Phone Orders — again because the parent’s add-to-cart re-creates them. |
wpo_add_configured_products_item_data | Extracts the composite parent’s readonly price from the WooCommerce cart price HTML using a regex pattern that accounts for the store’s decimal and thousand separators. |
How to Apply This Code
- Open Appearance → Theme File Editor or the Code Snippets plugin.
- Paste the full snippet into your child theme’s
functions.phpor create a new dedicated snippet. - Save and open Phone Orders. Search for a composite product and confirm it appears in results.
- Add it to the cart and verify the component selections, pricing, and parent-child structure display correctly.
- Load an existing order containing a composite product and confirm the configuration data loads intact.
⚠️ Always use a child theme or Code Snippets — parent theme files get overwritten on theme updates.
When Should You Use This Fix?
This fix applies whenever your store uses WooCommerce Composite Products and agents create or edit orders through Phone Orders PRO. Without it, composite products don’t appear in search, component data is lost on cart updates, and child items get added twice — producing incorrect order structures and prices.