Commit ffb46bfa543 for woocommerce
commit ffb46bfa5430ab6591242f922d62a306a8e70653
Author: Alefe Souza <contact@alefesouza.com>
Date: Tue Sep 29 22:31:30 2026 -0300
Fix variation attributes hidden when their value is in the parent product name (#68943)
diff --git a/plugins/woocommerce/changelog/31422-show-attribute-matching-parent-name b/plugins/woocommerce/changelog/31422-show-attribute-matching-parent-name
new file mode 100644
index 00000000000..2fb7e34409a
--- /dev/null
+++ b/plugins/woocommerce/changelog/31422-show-attribute-matching-parent-name
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show a variation attribute in the cart, checkout, and order item details when its value also appears in the parent product name.
diff --git a/plugins/woocommerce/includes/class-wc-order-item.php b/plugins/woocommerce/includes/class-wc-order-item.php
index 62d3450793c..af8b91ad6c4 100644
--- a/plugins/woocommerce/includes/class-wc-order-item.php
+++ b/plugins/woocommerce/includes/class-wc-order-item.php
@@ -362,7 +362,7 @@ class WC_Order_Item extends WC_Data implements ArrayAccess {
}
// Skip items with values already in the product details area of the product name.
- if ( null !== $decoded_item_name && wc_is_attribute_in_product_name( wp_specialchars_decode( $display_value, ENT_QUOTES ), $decoded_item_name ) ) {
+ if ( null !== $decoded_item_name && wc_is_attribute_in_product_name( wp_specialchars_decode( $display_value, ENT_QUOTES ), $decoded_item_name, $product ) ) {
continue;
}
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
index 0220719c95b..1fecb63bb49 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
@@ -285,7 +285,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
$display_value = wp_kses_post( rawurldecode( (string) $meta->value ) );
// Skip items with values already in the product details area of the product name.
- if ( $product && $product->is_type( ProductType::VARIATION ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) {
+ if ( $product && $product->is_type( ProductType::VARIATION ) && wc_is_attribute_in_product_name( $display_value, $order_item_name, $product ) ) {
return false;
}
diff --git a/plugins/woocommerce/includes/wc-attribute-functions.php b/plugins/woocommerce/includes/wc-attribute-functions.php
index 678a6032bd4..34a1d6af370 100644
--- a/plugins/woocommerce/includes/wc-attribute-functions.php
+++ b/plugins/woocommerce/includes/wc-attribute-functions.php
@@ -440,14 +440,46 @@ function wc_attributes_array_filter_variation( $attribute ) {
/**
* Check if an attribute is included in the attributes area of a variation name.
*
+ * A variation name is the parent product name followed by an optional attribute
+ * list ("Parent - Red, Large"). When the variation is given, only that attribute
+ * list is searched, so a parent name that ends with an attribute value
+ * ("Vienna Black") does not hide the "Black" attribute.
+ *
* @since 3.0.2
- * @param string $attribute Attribute value to check for.
- * @param string $name Product name to check in.
+ * @param string $attribute Attribute value to check for.
+ * @param string $name Product name to check in.
+ * @param WC_Product|null $product Variation the name belongs to, when known. Since 11.3.0.
* @return bool
*/
-function wc_is_attribute_in_product_name( $attribute, $name ) {
- $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) );
- return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name );
+function wc_is_attribute_in_product_name( $attribute, $name, $product = null ) {
+ $attributes_area = $name;
+
+ if ( $product instanceof WC_Product_Variation && $product->is_type( ProductType::VARIATION ) ) {
+ // The raw parent title, as the name is built from it without the `woocommerce_product_title` filter.
+ $parent_name = $product->get_parent_data()['title'] ?? '';
+
+ // Callers may pass the name raw or entity-decoded, so try both forms of the parent name.
+ foreach ( array_unique( array( $parent_name, wp_specialchars_decode( $parent_name, ENT_QUOTES ) ) ) as $prefix ) {
+ if ( '' !== $prefix && 0 === stripos( $name, $prefix ) ) {
+ $attributes_area = substr( $name, strlen( $prefix ) );
+ break;
+ }
+ }
+ }
+
+ $is_in_name = stristr( $attributes_area, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $attributes_area ), strrev( ' ' . $attribute ) );
+
+ /**
+ * Filters whether an attribute value is already shown in the attributes area of a variation name.
+ *
+ * @since 3.0.2
+ * @since 11.3.0 Added the `$product` parameter.
+ * @param bool $is_in_name Whether the attribute value was found in the name.
+ * @param string $attribute Attribute value checked for.
+ * @param string $name Product name checked in.
+ * @param WC_Product|null $product Variation the name belongs to, when known.
+ */
+ return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name, $product );
}
/**
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index c0829188dfd..cf28ab211ff 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -560,7 +560,7 @@ function wc_get_formatted_variation( $variation, $flat = false, $include_names =
}
// Do not list attributes already part of the variation name.
- if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) {
+ if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name, $product ? $product : null ) ) ) {
continue;
}
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index 45ae9bfe353..677d99a5b36 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -4576,7 +4576,7 @@ function wc_get_formatted_cart_item_data( $cart_item, $flat = false, $product_na
$value = rawurldecode( (string) $value );
// Check the display value against the title.
- if ( wc_is_attribute_in_product_name( wp_specialchars_decode( $value, ENT_QUOTES ), $product_name ) ) {
+ if ( wc_is_attribute_in_product_name( wp_specialchars_decode( $value, ENT_QUOTES ), $product_name, $cart_item['data'] ) ) {
continue;
}
diff --git a/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php b/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php
index 51bffe55e65..3b813091135 100644
--- a/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php
+++ b/plugins/woocommerce/src/Internal/ProductVariations/SelectedVariationName.php
@@ -80,7 +80,7 @@ class SelectedVariationName {
continue;
}
- if ( wc_is_attribute_in_product_name( $display_value, $product_name ) ) {
+ if ( wc_is_attribute_in_product_name( $display_value, $product_name, $product ) ) {
$has_attributes_in_name = true;
continue;
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index 82d2e772dc1..f2f7a221004 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -626,6 +626,48 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
}
}
+ /**
+ * @testdox Cart item metadata keeps an attribute whose value only appears in the parent product name.
+ */
+ public function test_formatted_cart_item_data_keeps_attribute_that_matches_the_parent_name(): void {
+ // Three attributes keep the attribute list out of the variation title, so it is just "Vienna Huge"
+ // and the "huge" size must not be treated as already shown.
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Huge',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ list( , $cart_item ) = $this->add_variation_to_cart(
+ $product,
+ $variation,
+ array(
+ 'attribute_pa_size' => 'huge',
+ 'attribute_pa_number' => '1',
+ 'attribute_pa_colour' => 'black',
+ )
+ );
+
+ $this->assertSame( 'Vienna Huge', WC()->cart->get_item_product_name( $cart_item ) );
+ $this->assertSame(
+ "size: huge\nnumber: 1\ncolour: black",
+ trim( wc_get_formatted_cart_item_data( $cart_item, true ) )
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
/**
* @testdox Cart item metadata dedup keys on the template-provided name regardless of name filters.
*/
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-order-item-product-test.php b/plugins/woocommerce/tests/php/includes/class-wc-order-item-product-test.php
index 602aecbbcae..42c6eb5f989 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-order-item-product-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-order-item-product-test.php
@@ -114,6 +114,41 @@ class WC_Order_Item_Product_Test extends WC_Unit_Test_Case {
WC_Helper_Order::delete_order( $this->order->get_id() );
}
+ /**
+ * @testdox get_formatted_meta_data keeps a variation attribute whose value only appears in the parent product name.
+ */
+ public function test_get_formatted_meta_data_keeps_attribute_that_matches_the_parent_name(): void {
+ // Three attributes keep the attribute list out of the variation title, so it is just "Vienna Huge".
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Huge',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ $item_id = $this->order->add_product( $variation, 1 );
+ $item = $this->order->get_item( $item_id );
+
+ $this->assertSame( 'Vienna Huge', $item->get_name() );
+ $this->assertSame(
+ array( 'huge', '1', 'black' ),
+ array_values( wp_list_pluck( $item->get_formatted_meta_data(), 'value' ) ),
+ 'Every selected attribute is listed when the item name shows none of them.'
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
/**
* @testdox 'doing it wrong' is thrown, if the Cost of Goods Sold feature is disabled.
*/
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller-test.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller-test.php
index f7baed2ce3e..5698d282998 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller-test.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller-test.php
@@ -124,6 +124,53 @@ class WC_REST_Order_V2_Controller_Test extends WC_REST_Unit_Test_case {
$this->assertEquals( $order->get_id(), $response->data['id'] );
}
+ /**
+ * Test that `order_item_display_meta` keeps a variation attribute whose value only appears in the
+ * parent product name.
+ */
+ public function test_order_item_display_meta_keeps_attribute_matching_the_parent_name() {
+ // Three attributes keep the attribute list out of the variation title, so it is just "Vienna Black"
+ // and the "black" colour must not be treated as already shown.
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Black',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ $order = new WC_Order();
+ $order->add_product( $variation, 1 );
+ $order->save();
+
+ $request = new WP_REST_Request( 'GET', '/wc/v2/orders/' . $order->get_id() );
+ $request->set_param( 'order_item_display_meta', 'true' );
+ $response = $this->server->dispatch( $request );
+
+ $this->assertEquals( 200, $response->get_status() );
+
+ $response_data = $response->get_data();
+ $line_item = current( $response_data['line_items'] );
+
+ $this->assertSame( 'Vienna Black', $line_item['name'] );
+ $this->assertSame(
+ array( 'pa_size', 'pa_number', 'pa_colour' ),
+ wp_list_pluck( $line_item['meta_data'], 'key' ),
+ 'Every selected attribute is returned when the item name shows none of them.'
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
/**
* Test that the `include_meta` param filters the `meta_data` prop correctly.
*/
diff --git a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
index 5d41654d0b2..fb2eec7f13e 100644
--- a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
@@ -52,6 +52,106 @@ class WC_Attribute_Functions_Test extends \WC_Unit_Test_Case {
parent::tearDown();
}
+ /**
+ * @testdox wc_is_attribute_in_product_name() should only search the attributes area of a variation name when the variation is given.
+ */
+ public function test_wc_is_attribute_in_product_name_ignores_the_parent_name_when_the_variation_is_given(): void {
+ // Three attributes keep the attribute list out of the variation title, so it is just "Vienna Black".
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Black',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ $this->assertSame( 'Vienna Black', $variation->get_name() );
+
+ $this->assertFalse(
+ wc_is_attribute_in_product_name( 'Black', 'Vienna Black', $variation ),
+ 'A value that only appears in the parent name is not shown by the variation name.'
+ );
+ $this->assertTrue(
+ wc_is_attribute_in_product_name( 'Black', 'Vienna Black - Huge, Black', $variation ),
+ 'A value in the attribute list after the parent name is shown by the variation name.'
+ );
+ $this->assertTrue(
+ wc_is_attribute_in_product_name( 'Black', 'Vienna Black' ),
+ 'Without the variation the whole name is searched, as before.'
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
+ /**
+ * @testdox wc_is_attribute_in_product_name() should match the parent name whether the variation name is raw or entity-decoded.
+ */
+ public function test_wc_is_attribute_in_product_name_matches_raw_and_decoded_parent_names(): void {
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Black & White',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'white',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ $this->assertFalse( wc_is_attribute_in_product_name( 'White', 'Black & White', $variation ), 'Raw name.' );
+ $this->assertFalse( wc_is_attribute_in_product_name( 'White', 'Black & White', $variation ), 'Decoded name.' );
+ $this->assertTrue( wc_is_attribute_in_product_name( 'White', 'Black & White - Huge, White', $variation ), 'Decoded name with the attribute list.' );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
+ /**
+ * @testdox wc_is_attribute_in_product_name() should match the parent name when a filter changes the product title.
+ */
+ public function test_wc_is_attribute_in_product_name_ignores_the_product_title_filter(): void {
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Black',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ $append_badge = function ( $title ) {
+ return $title . ' (New)';
+ };
+ add_filter( 'woocommerce_product_title', $append_badge );
+
+ try {
+ $this->assertFalse( wc_is_attribute_in_product_name( 'Black', 'Vienna Black', $variation ) );
+ } finally {
+ remove_filter( 'woocommerce_product_title', $append_badge );
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
/**
* Test wc_get_attribute_taxonomy_ids() function.
* Even empty arrays should be cached.
diff --git a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
index 86d4420734d..d04e2235d81 100644
--- a/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-product-functions-test.php
@@ -47,6 +47,39 @@ class WC_Product_Functions_Tests extends \WC_Unit_Test_Case {
parent::tearDown();
}
+ /**
+ * @testdox wc_get_formatted_variation() lists an attribute whose value only appears in the parent product name.
+ */
+ public function test_wc_get_formatted_variation_keeps_attribute_that_matches_the_parent_name(): void {
+ // Three attributes keep the attribute list out of the variation title, so it is just "Vienna Black"
+ // and the "black" colour must not be treated as already shown.
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Black',
+ array(
+ 'pa_size' => 'huge',
+ 'pa_number' => '1',
+ 'pa_colour' => 'black',
+ ),
+ array(
+ 'size' => array( 'small', 'huge' ),
+ 'number' => array( '0', '1' ),
+ 'colour' => array( 'black', 'white' ),
+ )
+ );
+
+ try {
+ $this->assertSame( 'Vienna Black', $variation->get_name() );
+ $this->assertSame(
+ 'size: huge, number: 1, colour: black',
+ wc_get_formatted_variation( $variation, true, true, true ),
+ 'Every attribute is listed when the variation name shows none of them.'
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
/**
* @testdox If 'wc_get_price_excluding_tax' gets an order as argument, it passes the order customer to 'WC_Tax::get_rates'.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php
index ea921e08f9d..56f0f14c597 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductVariations/SelectedVariationNameTest.php
@@ -74,6 +74,69 @@ class SelectedVariationNameTest extends WC_Unit_Test_Case {
}
}
+ /**
+ * @testdox Selected Any attributes are kept when their value also appears in the parent product name.
+ *
+ * @dataProvider parent_name_collision_provider
+ *
+ * @param array<string, string> $variation_attributes Stored variation attributes.
+ * @param array<string, array<int, string>> $attribute_terms Terms to create for each attribute.
+ * @param array<string, string> $selected_attributes Selected cart attributes.
+ * @param string $expected_name Expected contextual name.
+ */
+ public function test_get_product_name_keeps_attributes_matching_the_parent_name( array $variation_attributes, array $attribute_terms, array $selected_attributes, string $expected_name ): void {
+ // Every attribute is "Any", so the variation title is just the parent name and shows none of the
+ // selected values -- including the colour the parent name happens to end with.
+ list( $product, $variation ) = WC_Helper_Product::create_variation_product_with_global_attributes(
+ 'Vienna Black',
+ $variation_attributes,
+ $attribute_terms
+ );
+
+ try {
+ $this->assertSame( 'Vienna Black', $variation->get_name() );
+ $this->assertSame(
+ $expected_name,
+ $this->sut->get_product_name( $variation, $selected_attributes ),
+ 'The selected value is listed even though the parent name ends with it.'
+ );
+ } finally {
+ $variation->delete( true );
+ $product->delete( true );
+ }
+ }
+
+ /**
+ * Provides variations whose parent name ends with a selected attribute value.
+ *
+ * @return array<string, array{0: array<string, string>, 1: array<string, array<int, string>>, 2: array<string, string>, 3: string}>
+ */
+ public static function parent_name_collision_provider(): array {
+ return array(
+ 'only attribute is Any' => array(
+ array( 'pa_colour' => '' ),
+ array( 'colour' => array( 'black', 'white' ) ),
+ array( 'attribute_pa_colour' => 'black' ),
+ 'Vienna Black - black',
+ ),
+ 'every attribute is Any' => array(
+ array(
+ 'pa_colour' => '',
+ 'pa_number' => '',
+ ),
+ array(
+ 'colour' => array( 'black', 'white' ),
+ 'number' => array( '0', '1' ),
+ ),
+ array(
+ 'attribute_pa_colour' => 'black',
+ 'attribute_pa_number' => '1',
+ ),
+ 'Vienna Black - black, 1',
+ ),
+ );
+ }
+
/**
* @testdox Data stores without the shared title policy keep the stored variation name.
*/