get_title(), $main_image, $image_index ) : $alt_text;
if ( ! is_array( $full_src ) ) {
$full_src = [ '', '', '' ];
}
/**
* Filters the attributes for the image markup.
*
* @since 3.3.2
*
* @param array $image_attributes Attributes for the image markup.
*/
$image_params = apply_filters(
'woocommerce_gallery_image_html_attachment_image_params',
[
'title' => _wp_specialchars( get_post_field( 'post_title', $attachment_id ), ENT_QUOTES, 'UTF-8', true ),
'data-caption' => _wp_specialchars( get_post_field( 'post_excerpt', $attachment_id ), ENT_QUOTES, 'UTF-8', true ),
'data-src' => esc_url( $full_src[0] ),
'data-large_image' => esc_url( $full_src[0] ),
'data-large_image_width' => esc_attr( $full_src[1] ),
'data-large_image_height' => esc_attr( $full_src[2] ),
'class' => esc_attr( $main_image ? 'wp-post-image' : '' ),
'alt' => esc_attr( $alt_text ),
],
$attachment_id,
$image_size,
$main_image
);
if ( isset( $image_params['title'] ) ) {
unset( $image_params['title'] );
}
$image = wp_get_attachment_image(
$attachment_id,
$image_size,
false,
$image_params
);
return $image;
}
/**
* Get the HTML for a variation gallery thumbnail image.
*
* Wraps the image in a container div and optionally adds a video-specific class
* if a video link is associated with the image. The final HTML is passed through
* a filter for customization.
*
* @param int $image_id The attachment/image ID.
* @param \WC_Product $product The WooCommerce product object.
* @param string $size The image size to output. Default 'thumbnail'.
*
* @return string Filtered HTML for the thumbnail image.
*/
public static function get_thumbnail_attachment_html( $image_id, $product, $size = 'thumbnail' ) {
$props = self::product_attachment_props( $image_id, $product );
$add_video_class = ! empty( $props['rtsb_vg_video_link'] ) ? ' rtsb-vs-video' : '';
ob_start();
?>
get_id();
$attachment_ids = $product->get_gallery_image_ids();
$post_thumbnail_id = $product->get_image_id();
$images = [];
$post_thumbnail_id = (int) apply_filters( 'rtsb/vg/post/thumbnail/id', $post_thumbnail_id, $attachment_ids, $product );
$attachment_ids = (array) apply_filters( 'rtsb/vg/attachment/ids', $attachment_ids, $post_thumbnail_id, $product );
if ( ! empty( $post_thumbnail_id ) ) {
array_unshift( $attachment_ids, $post_thumbnail_id );
}
if ( is_array( $attachment_ids ) && ! empty( $attachment_ids ) ) {
foreach ( $attachment_ids as $i => $image_id ) {
if ( $image_id ) {
$images[ $i ] = self::product_attachment_props( $image_id, $product );
}
}
}
set_transient( $transient_name, $images, 12 * HOUR_IN_SECONDS );
Cache::set_transient_cache_key( $transient_name );
return apply_filters( 'rtsb/vg/get/gallery/images', $images, $product_id );
}
/**
* Build (and cache) the gallery image props for a single variation.
*
* Assembles the image-ID list for one variation only — the per-variation logic
* previously executed for every variation inside the
* `woocommerce_available_variation` filter — so galleries can be fetched
* on-demand over AJAX instead of being built in bulk on page load.
*
* The result is cached in a transient keyed by variation ID and is always a
* sequential array (built with `$images[]`) so it JSON-encodes to a JS array.
*
* @param int $product_id Parent (variable) product ID.
* @param int $variation_id Variation ID whose gallery should be built.
*
* @return array Sequential array of image prop arrays.
*/
public static function get_variation_gallery( $product_id, $variation_id ) {
$variation_id = absint( $variation_id );
$product_id = absint( $product_id );
$transient_name = self::get_transient_name( $variation_id, 'variation-images' );
$cached = $transient_name ? get_transient( $transient_name ) : false;
if ( ! empty( $cached ) && is_array( $cached ) ) {
return apply_filters( 'rtsb/vg/get/variation/gallery', $cached, $product_id, $variation_id );
}
$variation = wc_get_product( $variation_id );
if ( ! $variation ) {
return [];
}
$parent_product = $product_id ? wc_get_product( $product_id ) : false;
$has_variation_gallery_images = (bool) get_post_meta( $variation_id, 'rtsb_vg_images', true );
if ( $has_variation_gallery_images ) {
$gallery_images = (array) get_post_meta( $variation_id, 'rtsb_vg_images', true );
} elseif ( $parent_product ) {
$gallery_images = (array) $parent_product->get_gallery_image_ids();
} else {
$gallery_images = (array) $variation->get_gallery_image_ids();
}
$variation_image_id = absint( $variation->get_image_id() );
if ( ! empty( $variation_image_id ) ) {
array_unshift( $gallery_images, $variation_image_id );
}
$gallery_images = array_values( array_unique( array_filter( array_map( 'absint', $gallery_images ) ) ) );
$images = [];
foreach ( $gallery_images as $image_id ) {
if ( $image_id ) {
// Always append so the result stays a sequential (JS) array.
$images[] = self::product_attachment_props( $image_id, $variation );
}
}
if ( $transient_name ) {
set_transient( $transient_name, $images, HOUR_IN_SECONDS * 12 );
Cache::set_transient_cache_key( $transient_name );
}
return apply_filters( 'rtsb/vg/get/variation/gallery', $images, $product_id, $variation_id );
}
/**
* Resolve the default variation ID for a variable product.
*
* Returns the variation that WooCommerce's default attributes fully resolve to,
* so the matching gallery can be rendered server-side on initial page load.
*
* @param \WC_Product $product Variable product object.
*
* @return int Variation ID, or 0 when no single default variation resolves.
*/
public static function get_default_variation_id( $product ) {
if ( ! ( $product instanceof WC_Product ) || ! $product->is_type( 'variable' ) ) {
return 0;
}
$default_attributes = $product->get_default_attributes();
if ( empty( $default_attributes ) ) {
return 0;
}
$match_attributes = [];
foreach ( $default_attributes as $key => $value ) {
$match_attributes[ 'attribute_' . $key ] = $value;
}
$data_store = \WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $product, $match_attributes );
return absint( $variation_id );
}
/**
* Get the attachment IDs to render for a product's initial gallery paint.
*
* When a default variation resolves, returns that variation's gallery image IDs
* (and warms its transient as a side effect of get_variation_gallery()). Otherwise
* falls back to the parent product's featured + gallery images.
*
* @param \WC_Product $product Product object.
*
* @return array {
* @type int[] $image_ids Sequential attachment IDs to render.
* @type \WC_Product $context Product/variation object to use as render context.
* }
*/
public static function get_initial_gallery_render( $product ) {
$context = $product;
$image_ids = [];
if ( $product instanceof WC_Product ) {
$default_variation_id = self::get_default_variation_id( $product );
if ( $default_variation_id ) {
$variation_props = self::get_variation_gallery( $product->get_id(), $default_variation_id );
foreach ( (array) $variation_props as $prop ) {
if ( ! empty( $prop['image_id'] ) ) {
$image_ids[] = absint( $prop['image_id'] );
}
}
if ( ! empty( $image_ids ) ) {
$variation = wc_get_product( $default_variation_id );
if ( $variation ) {
$context = $variation;
}
}
}
}
if ( empty( $image_ids ) && ( $product instanceof WC_Product ) ) {
$post_thumbnail_id = $product->get_image_id();
$attachment_ids = $product->get_gallery_image_ids();
if ( $post_thumbnail_id ) {
$image_ids[] = absint( $post_thumbnail_id );
}
if ( is_array( $attachment_ids ) && count( $attachment_ids ) ) {
foreach ( $attachment_ids as $attachment_id ) {
$image_ids[] = absint( $attachment_id );
}
}
}
$image_ids = array_values( array_unique( array_filter( $image_ids ) ) );
return [
'image_ids' => $image_ids,
'context' => $context,
];
}
/**
* Flush all gallery transients (default + every variation) for a product.
*
* Used when a product or one of its variations is saved so stale gallery
* props are not served from cache.
*
* @param int $product_id Parent product ID.
*
* @return void
*/
public static function flush_product_gallery_transients( $product_id ) {
$product_id = absint( $product_id );
if ( ! $product_id ) {
return;
}
self::delete_transients( $product_id, 'default-images' );
$product = wc_get_product( $product_id );
if ( ! $product || ! $product->is_type( 'variable' ) ) {
return;
}
foreach ( $product->get_children() as $variation_id ) {
self::delete_transients( $variation_id, 'variation-images' );
}
}
/**
* Flush variation gallery transients that reference a given attachment.
*
* Invoked when an attachment is deleted so variations whose galleries include
* the removed image rebuild their props on next request.
*
* @param int $attachment_id Deleted attachment ID.
*
* @return void
*/
public static function flush_attachment_gallery_transients( $attachment_id ) {
global $wpdb;
$attachment_id = absint( $attachment_id );
if ( ! $attachment_id ) {
return;
}
// Flush the owning product (featured/parent) galleries when determinable.
$parent_id = wp_get_post_parent_id( $attachment_id );
if ( $parent_id ) {
self::flush_product_gallery_transients( $parent_id );
}
// Flush any variation whose custom gallery meta references this attachment.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
'rtsb_vg_images'
)
);
if ( empty( $rows ) ) {
return;
}
foreach ( $rows as $row ) {
$image_ids = maybe_unserialize( $row->meta_value );
if ( ! is_array( $image_ids ) ) {
continue;
}
$image_ids = array_map( 'absint', $image_ids );
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( in_array( $attachment_id, $image_ids, true ) ) {
self::delete_transients( $row->post_id, 'variation-images' );
}
}
}
/**
* Helper: Get all images transient name for specific variation/product
*
* @param int $id unique id.
* @param string $type transient type.
*
* @return string
*/
public static function get_transient_name( $id, $type ) {
if ( 'default-images' === $type ) {
$id = self::wpml_get_original_variation_id( $id );
$transient_name = sprintf( 'rtsb_vg_default_images_%d', $id );
} elseif ( 'variation-images' === $type ) {
$id = self::wpml_get_original_variation_id( $id );
$transient_name = sprintf( 'rtsb_vg_variation_images_%d', $id );
} elseif ( 'sizes' === $type ) {
$transient_name = sprintf( 'rtsb_vg_variation_image_sizes_%d', $id );
} elseif ( 'variation' === $type ) {
$transient_name = sprintf( 'rtsb_vg_variation_%d', $id );
} else {
$transient_name = false;
}
return apply_filters( 'rtsb/vg/transient/name', $transient_name, $type, $id );
}
/**
* Helper: Delete all transient
*
* @param int $product_id Product id.
* @param string $type Transient type.
*/
public static function delete_transients( $product_id = false, $type = '' ) {
if ( $product_id ) {
if ( $type ) {
$default_transient_name = self::get_transient_name( $product_id, $type );
delete_transient( $default_transient_name );
} else {
$default_transient_name = self::get_transient_name( $product_id, 'default-images' );
delete_transient( $default_transient_name );
}
}
}
/**
* Helper: WPML - Get original variation ID
*
* If WPML is active and this is a translated variaition, get the original ID.
*
* @param int $id object id.
*
* @return int
*/
public static function wpml_get_original_variation_id( $id ) {
$wpml_original_variation_id = get_post_meta( $id, '_wcml_duplicate_of_variation', true );
if ( $wpml_original_variation_id ) {
$id = $wpml_original_variation_id;
}
return $id;
}
}