Commit 9d39c41a6cc for woocommerce
commit 9d39c41a6cc8ba1bd74451e6069a7c9baca60848
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date: Tue Jun 2 15:03:22 2026 +0200
Product Gallery block: show parent gallery for single-image variations (#65267)
* Show parent gallery for single-image variations in Product Gallery block
* Add changelog entry
* Rename hero to featured and clarify variation gallery comments
* Refactor variation gallery ID filtering
* Clarify placeholder image ID fallback
* Refactor parent gallery extras filtering
* Refactor product gallery variation test setup
diff --git a/plugins/woocommerce/changelog/product-gallery-block-variation-image-decision-tree b/plugins/woocommerce/changelog/product-gallery-block-variation-image-decision-tree
new file mode 100644
index 00000000000..15980b5dc9b
--- /dev/null
+++ b/plugins/woocommerce/changelog/product-gallery-block-variation-image-decision-tree
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Show parent gallery extras alongside single-image variations in the Product Gallery block.
diff --git a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
index abe9b2300c6..bd8c7b57695 100644
--- a/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
+++ b/plugins/woocommerce/src/Blocks/Utils/ProductGalleryUtils.php
@@ -159,18 +159,20 @@ class ProductGalleryUtils {
_prime_post_caches( $variations );
}
- $parent_image_ids = array_values(
- array_filter(
- array_map( 'intval', self::get_product_gallery_image_ids( $product ) ),
- static function ( $id ) {
- return $id > 0 && wp_attachment_is_image( $id );
- }
- )
- );
+ // 0 is placeholder image ID.
+ $parent_featured_id = 0;
+ $product_image_id = (int) $product->get_image_id();
+ if ( $product_image_id && wp_attachment_is_image( $product_image_id ) ) {
+ $parent_featured_id = $product_image_id;
+ }
+
+ $parent_gallery_ids = array_map( 'intval', $product->get_gallery_image_ids() );
+ $parent_gallery_ids = array_filter( $parent_gallery_ids, 'wp_attachment_is_image' );
+ $parent_gallery_extras = array_values( array_diff( $parent_gallery_ids, array( $parent_featured_id ) ) );
foreach ( $variations as $variation_id ) {
$variation_id = (int) $variation_id;
- $entry = self::build_variation_gallery_entry( $variation_id, $parent_image_ids );
+ $entry = self::build_variation_gallery_entry( $variation_id, $parent_featured_id, $parent_gallery_extras );
if ( null !== $entry ) {
$variation_gallery_data[ $variation_id ] = $entry;
@@ -182,61 +184,75 @@ class ProductGalleryUtils {
/**
* Build the gallery payload for a single variation, or null when the
- * variation has no images or isn't a real variation.
+ * post isn't a real variation.
+ *
+ * Decision tree (variation chosen):
+ * - no variation images → parent featured + parent gallery
+ * - own featured only → variation featured + parent gallery extras
+ * - own featured + gallery (flag on) → variation images only
+ * - gallery only, no own featured (potential AVI shape) → parent featured + variation gallery
*
- * @param int $variation_id Variation post ID.
- * @param int[] $parent_image_ids Parent product's full gallery (featured + extras),
- * used as fallback when the variation has no images.
+ * @param int $variation_id Variation post ID.
+ * @param int $parent_featured_id Parent product's featured image ID (0 if missing/invalid).
+ * @param int[] $parent_gallery_extras Parent gallery image IDs, with the featured filtered out.
* @return array<string, mixed>|null
*/
- private static function build_variation_gallery_entry( int $variation_id, array $parent_image_ids ): ?array {
+ private static function build_variation_gallery_entry( int $variation_id, int $parent_featured_id, array $parent_gallery_extras ): ?array {
$variation = wc_get_product( $variation_id );
if ( ! $variation instanceof \WC_Product_Variation ) {
return null;
}
- $variation_image_id = (int) $variation->get_image_id();
- $variation_image_valid = $variation_image_id && wp_attachment_is_image( $variation_image_id );
- $parent_fallback = ! empty( $parent_image_ids )
- ? array(
- 'image_id' => $parent_image_ids[0],
- 'image_ids' => $parent_image_ids,
- )
- : array(
- 'image_id' => 0,
- 'image_ids' => array( 0 ),
+ $featured_id = (int) $variation->get_image_id();
+ $featured_valid = $featured_id && wp_attachment_is_image( $featured_id );
+
+ $variation_gallery_ids = array();
+ if ( VariationGalleryPackage::is_enabled() ) {
+ $variation_gallery_ids = array_map( 'intval', $variation->get_gallery_image_ids() );
+ $variation_gallery_ids = array_filter( $variation_gallery_ids, 'wp_attachment_is_image' );
+ $variation_gallery_ids = array_values( $variation_gallery_ids );
+ }
+
+ // No images from variation - full parent fallback.
+ if ( ! $featured_valid && empty( $variation_gallery_ids ) ) {
+ $parent_image_ids = array_values(
+ array_filter( array_merge( array( $parent_featured_id ), $parent_gallery_extras ) )
);
- if ( ! VariationGalleryPackage::is_enabled() ) {
- if ( $variation_image_valid ) {
+ if ( empty( $parent_image_ids ) ) {
return array(
- 'image_id' => $variation_image_id,
- 'image_ids' => array( $variation_image_id ),
+ 'image_id' => 0,
+ 'image_ids' => array( 0 ),
);
}
- return $parent_fallback;
- }
-
- $image_ids = self::get_variation_gallery_image_ids( $variation );
- if ( empty( $image_ids ) ) {
- return $parent_fallback;
+ return array(
+ 'image_id' => $parent_image_ids[0],
+ 'image_ids' => $parent_image_ids,
+ );
}
- // Prefer variation-owned images over the parent fallback.
- if ( $variation_image_valid ) {
- $selected_image_id = $variation_image_id;
- } else {
- $selected_image_id = $image_ids[0];
- }
+ // Variation has featured image and gallery - full variation gallery.
+ if ( ! empty( $variation_gallery_ids ) ) {
+ $featured = $featured_valid ? $featured_id : $variation_gallery_ids[0];
+ $image_ids = array_values(
+ array_unique( array_merge( array( $featured ), $variation_gallery_ids ) )
+ );
- if ( ! in_array( $selected_image_id, $image_ids, true ) ) {
- array_unshift( $image_ids, $selected_image_id );
+ return array(
+ 'image_id' => $featured,
+ 'image_ids' => $image_ids,
+ );
}
+ // Variation has only featured image - variation featured and parent gallery.
+ $image_ids = array_values(
+ array_unique( array_merge( array( $featured_id ), $parent_gallery_extras ) )
+ );
+
return array(
- 'image_id' => $selected_image_id,
+ 'image_id' => $featured_id,
'image_ids' => $image_ids,
);
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductGalleryUtilsTest.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductGalleryUtilsTest.php
index 128fe7c1cb3..519ad9c1e5b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductGalleryUtilsTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/ProductGalleryUtilsTest.php
@@ -1,4 +1,5 @@
<?php
+declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\Utils;
@@ -218,6 +219,179 @@ class ProductGalleryUtilsTest extends \WP_UnitTestCase {
);
}
+ /**
+ * Variation has only its own featured image (no gallery) → the
+ * variation featured replaces the parent's hero, parent gallery extras
+ * stay. Applies whether the feature flag is on or off.
+ */
+ public function test_get_product_variation_gallery_data_case_3_single_image_appends_parent_gallery_extras() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $parent_gallery_extra_a = $this->create_image_attachment( 'Parent Gallery A', 'parent-gallery-a.jpg' );
+ $parent_gallery_extra_b = $this->create_image_attachment( 'Parent Gallery B', 'parent-gallery-b.jpg' );
+ $variation_featured_id = $this->create_image_attachment( 'Variation Featured', 'variation-featured.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $parent_gallery_extra_a, $parent_gallery_extra_b ),
+ $variation_featured_id
+ );
+
+ $this->assertSame( $variation_featured_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $variation_featured_id, $parent_gallery_extra_a, $parent_gallery_extra_b ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * Variation has its own featured plus gallery images (feature flag
+ * on) → the variation's images replace the parent's entirely.
+ */
+ public function test_get_product_variation_gallery_data_case_4_multiple_images_replaces_parent_set() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $parent_gallery_extra = $this->create_image_attachment( 'Parent Gallery', 'parent-gallery.jpg' );
+ $variation_featured_id = $this->create_image_attachment( 'Variation Featured', 'variation-featured.jpg' );
+ $variation_gallery_id_a = $this->create_image_attachment( 'Variation Gallery A', 'variation-gallery-a.jpg' );
+ $variation_gallery_id_b = $this->create_image_attachment( 'Variation Gallery B', 'variation-gallery-b.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $parent_gallery_extra ),
+ $variation_featured_id,
+ array( $variation_gallery_id_a, $variation_gallery_id_b )
+ );
+
+ $this->assertSame( $variation_featured_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $variation_featured_id, $variation_gallery_id_a, $variation_gallery_id_b ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * (Edge case - legacy AVI shape): variation has gallery images but no own featured →
+ * parent featured anchors the set, variation gallery follows. No parent
+ * gallery extras appear — the variation owns the rest of the lineup.
+ */
+ public function test_get_product_variation_gallery_data_case_5_avi_shape_uses_parent_featured_plus_variation_gallery() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $parent_gallery_extra = $this->create_image_attachment( 'Parent Gallery Extra', 'parent-gallery-extra.jpg' );
+ $variation_gallery_id_a = $this->create_image_attachment( 'Variation Gallery A', 'variation-gallery-a.jpg' );
+ $variation_gallery_id_b = $this->create_image_attachment( 'Variation Gallery B', 'variation-gallery-b.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $parent_gallery_extra ),
+ 0,
+ array( $variation_gallery_id_a, $variation_gallery_id_b )
+ );
+
+ $this->assertSame( $parent_featured_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $parent_featured_id, $variation_gallery_id_a, $variation_gallery_id_b ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * Variation has no images of its own → full parent gallery
+ * (featured + extras) is used as a fallback.
+ */
+ public function test_get_product_variation_gallery_data_case_2_no_variation_images_uses_full_parent_gallery() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $parent_gallery_extra = $this->create_image_attachment( 'Parent Gallery Extra', 'parent-gallery-extra.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $parent_gallery_extra )
+ );
+
+ $this->assertSame( $parent_featured_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $parent_featured_id, $parent_gallery_extra ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * Feature flag off: Variation gallery is treated as empty even if rows exist in postmeta,
+ * so the single-image rule applies (variation featured + parent gallery extras).
+ */
+ public function test_get_product_variation_gallery_data_case_3_applies_with_feature_flag_off() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $parent_gallery_extra = $this->create_image_attachment( 'Parent Gallery Extra', 'parent-gallery-extra.jpg' );
+ $variation_featured_id = $this->create_image_attachment( 'Variation Featured', 'variation-featured.jpg' );
+ $variation_gallery_id_a = $this->create_image_attachment( 'Variation Gallery A (ignored)', 'variation-gallery-a.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $parent_gallery_extra ),
+ $variation_featured_id,
+ array( $variation_gallery_id_a ),
+ 'no'
+ );
+
+ $this->assertSame( $variation_featured_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $variation_featured_id, $parent_gallery_extra ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * The variation featured is also present in the parent gallery — output
+ * must dedup so the image doesn't render twice in a row.
+ */
+ public function test_get_product_variation_gallery_data_dedups_variation_featured_overlapping_parent_gallery() {
+ $parent_featured_id = $this->create_image_attachment( 'Parent Featured', 'parent-featured.jpg' );
+ $shared_id = $this->create_image_attachment( 'Shared Image', 'shared.jpg' );
+ $parent_gallery_extra = $this->create_image_attachment( 'Parent Gallery Extra', 'parent-gallery-extra.jpg' );
+
+ $entry = $this->create_variation_gallery_entry(
+ $parent_featured_id,
+ array( $shared_id, $parent_gallery_extra ),
+ $shared_id
+ );
+
+ $this->assertSame( $shared_id, $entry['image_id'] );
+ $this->assertSame(
+ array( $shared_id, $parent_gallery_extra ),
+ $entry['image_ids']
+ );
+ }
+
+ /**
+ * Create a variation gallery fixture and return the selected variation entry.
+ *
+ * @param int $parent_featured_id Parent product featured image ID.
+ * @param int[] $parent_gallery_ids Parent product gallery image IDs.
+ * @param int $variation_featured_id Variation featured image ID.
+ * @param int[] $variation_gallery_ids Variation gallery image IDs.
+ * @param string $feature_flag Variation gallery feature flag value.
+ * @return array<string, mixed>
+ */
+ private function create_variation_gallery_entry(
+ int $parent_featured_id,
+ array $parent_gallery_ids = array(),
+ int $variation_featured_id = 0,
+ array $variation_gallery_ids = array(),
+ string $feature_flag = 'yes'
+ ): array {
+ update_option( \Automattic\WooCommerce\Internal\VariationGallery\Package::ENABLE_OPTION_NAME, $feature_flag );
+
+ $variable_product = \WC_Helper_Product::create_variation_product();
+ $variable_product->set_image_id( $parent_featured_id );
+ $variable_product->set_gallery_image_ids( $parent_gallery_ids );
+ $variable_product->save();
+
+ $variation = wc_get_product( $variable_product->get_children()[0] );
+ $variation->set_image_id( $variation_featured_id );
+ $variation->set_gallery_image_ids( $variation_gallery_ids );
+ $variation->save();
+
+ return ProductGalleryUtils::get_product_variation_gallery_data( $variable_product )[ $variation->get_id() ];
+ }
+
/**
* Create a real image attachment that passes `wp_attachment_is_image()`.
*