, * minimum_price: string, * has_prices: bool * } `ranges` always carries all three sub-ranges, empty ones as empty strings. * `minimum_price` is the filtered minimum BEFORE any decimal formatting — the * value a caller should use for the parent's own `price` field, so it can apply * the request's own `dp` precision. `has_prices` is false when no visible child * carries a price at all. */ public static function for( WC_Product_Variable $product, string $format = self::FORMAT_DECIMAL ): array { $as_decimal = self::FORMAT_DECIMAL === $format; $decimals = \function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2; $collected = array_fill_keys( self::FIELDS, array() ); $child_ids = self::visible_child_ids( $product ); // One bulk posts+postmeta prime for the whole family: without it every // wc_get_product() below pays its own single-id posts + postmeta pair — // ~13 queries per variable product on a catalog page (wcpos#1569, the // dominant shape in the dev-next slow log). Prices live in postmeta; // term caches are not read here. if ( \count( $child_ids ) > 1 ) { _prime_post_caches( $child_ids, false, true ); } foreach ( $child_ids as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( ! $variation ) { continue; } $price = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally invoking WC core filter. 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); if ( '' === $price || null === $price ) { continue; } $regular_price = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally invoking WC core filter. 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); $sale_price = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally invoking WC core filter. 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); $formatted_price = wc_format_decimal( $price, $decimals ); $formatted_regular_price = wc_format_decimal( $regular_price, $decimals ); $formatted_sale_price = wc_format_decimal( $sale_price, $decimals ); // The active price keeps the child's own string in BOTH renderings; the // decimal rendering formats it once at the end, after the min/max filter, // so a filtered minimum is never rounded twice. $collected['price'][ $variation_id ] = (string) $price; if ( $as_decimal ) { $collected['regular_price'][ $variation_id ] = $formatted_regular_price; } elseif ( '' !== $regular_price && null !== $regular_price ) { $collected['regular_price'][ $variation_id ] = (string) $regular_price; } if ( '' !== $sale_price && null !== $sale_price && $formatted_sale_price !== $formatted_regular_price && $formatted_sale_price === $formatted_price ) { $collected['sale_price'][ $variation_id ] = $as_decimal ? $formatted_sale_price : (string) $sale_price; } } foreach ( $collected as $field => $values ) { asort( $values, SORT_NUMERIC ); $collected[ $field ] = $values; } $price_range = self::apply_range_filter( 'woocommerce_get_variation_price', self::min_max( $collected['price'] ), $product ); $minimum_price = $price_range['min']; if ( $as_decimal ) { $price_range = array( 'min' => '' === $price_range['min'] ? '' : wc_format_decimal( $price_range['min'], $decimals ), 'max' => '' === $price_range['max'] ? '' : wc_format_decimal( $price_range['max'], $decimals ), ); } $ranges = array( 'price' => $price_range, 'regular_price' => self::apply_range_filter( 'woocommerce_get_variation_regular_price', self::min_max( $collected['regular_price'] ), $product ), 'sale_price' => self::apply_range_filter( 'woocommerce_get_variation_sale_price', self::min_max( $collected['sale_price'] ), $product ), ); return array( 'ranges' => $ranges, 'minimum_price' => $minimum_price, 'has_prices' => self::any_range_populated( $ranges ), ); } /** * The visible child variation ids, minus the ones hidden from the POS. * * WooCommerce's visible-children resolution applies the store's WEB visibility * rules, NOT the WCPOS `online_only` exclusion — a child hidden from the POS * would otherwise leak its price into the served range. The subtraction is gated * on the feature toggle inside Pos_Visibility, which returns an empty list when * the toggle is off. * * @param WC_Product_Variable $product Variable product. * * @return array */ private static function visible_child_ids( WC_Product_Variable $product ): array { return ( new Pos_Visibility() )->filter_visible_children( $product->get_visible_children() ); } /** * Apply a WooCommerce min/max variation price filter to a computed range. * * An empty end is left alone: an absent sub-range must stay absent, and handing * `''` to a repricing filter would invite a `0.00` back. * * @param string $hook_name WooCommerce price range filter name. * @param array{min: string, max: string} $price_range Computed range. * @param WC_Product_Variable $product Variable product. * * @return array{min: string, max: string} */ private static function apply_range_filter( string $hook_name, array $price_range, WC_Product_Variable $product ): array { if ( '' !== $price_range['min'] ) { $price_range['min'] = (string) apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally invoking WC core filter. $hook_name, $price_range['min'], $product, 'min', false ); } if ( '' !== $price_range['max'] ) { $price_range['max'] = (string) apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally invoking WC core filter. $hook_name, $price_range['max'], $product, 'max', false ); } return $price_range; } /** * Convert a numerically-sorted variation price list to a min/max range. * * @param array $prices Prices keyed by variation id, sorted ascending. * * @return array{min: string, max: string} */ private static function min_max( array $prices ): array { if ( empty( $prices ) ) { return array( 'min' => '', 'max' => '', ); } return array( 'min' => (string) reset( $prices ), 'max' => (string) end( $prices ), ); } /** * Whether any sub-range carries a value. * * @param array $ranges Computed sub-ranges. */ private static function any_range_populated( array $ranges ): bool { foreach ( $ranges as $range ) { if ( '' !== $range['min'] || '' !== $range['max'] ) { return true; } } return false; } }