Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.50% covered (warning)
62.50%
25 / 40
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Wc_Block_Helpers
62.50% covered (warning)
62.50%
25 / 40
0.00% covered (danger)
0.00%
0 / 2
38.04
0.00% covered (danger)
0.00%
0 / 1
 get_currency_display
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
9.13
 get_catalog_price_extents
43.48% covered (danger)
43.48%
10 / 23
0.00% covered (danger)
0.00%
0 / 1
28.06
1<?php
2/**
3 * WooCommerce helper utilities shared between `filter-wc-*` blocks.
4 *
5 * @package automattic/jetpack-search
6 */
7
8namespace Automattic\Jetpack\Search;
9
10/**
11 * Render-time helpers for the WC price / price-slider blocks. Separate from
12 * `Search_Blocks` so the WC-touching code has a dedicated home.
13 */
14class Wc_Block_Helpers {
15
16    const PRICE_EXTENTS_TRANSIENT = 'jetpack_search_wc_price_extents';
17    const PRICE_EXTENTS_TTL_SEC   = 5 * MINUTE_IN_SECONDS;
18
19    /**
20     * Currency adornment (symbol + position) for a price block. Empty author
21     * values fall through to WC settings; `$`/`left` for plain WP installs.
22     *
23     * @param string $author_symbol   '' to defer to WC.
24     * @param string $author_position '' to defer to WC.
25     * @return array{symbol:string,position:string} symbol clipped to 2 chars.
26     */
27    public static function get_currency_display( string $author_symbol, string $author_position ): array {
28        $symbol   = $author_symbol;
29        $position = $author_position;
30
31        if ( '' === $symbol && function_exists( 'get_woocommerce_currency_symbol' ) ) {
32            // @phan-suppress-next-line PhanUndeclaredFunction
33            $wc_symbol = (string) get_woocommerce_currency_symbol();
34            // WC returns HTML entities (`&#36;`, `&euro;`). Decode once so `mb_substr`
35            // sees a character (not half an entity) and `esc_html` round-trips cleanly.
36            $symbol = html_entity_decode( $wc_symbol, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
37        }
38        if ( '' === $symbol ) {
39            $symbol = '$';
40        }
41
42        if ( '' === $position ) {
43            $wc_pos   = (string) get_option( 'woocommerce_currency_pos', 'left' );
44            $position = ( 'right' === $wc_pos || 'right_space' === $wc_pos ) ? 'right' : 'left';
45        }
46        if ( ! in_array( $position, array( 'left', 'right' ), true ) ) {
47            $position = 'left';
48        }
49
50        $symbol_short = function_exists( 'mb_substr' ) ? mb_substr( $symbol, 0, 2 ) : substr( $symbol, 0, 2 );
51
52        return array(
53            'symbol'   => $symbol_short,
54            'position' => $position,
55        );
56    }
57
58    /**
59     * Catalog-wide price extents from WC's `wc_product_meta_lookup`. Bounds
60     * stay stable for the page (matches WC's slider — applying filters narrows
61     * products without shrinking the track). Transient-cached; null-extents
62     * are cached too so broken setups don't re-run the query.
63     *
64     * @return array{min:float|null,max:float|null}
65     */
66    public static function get_catalog_price_extents(): array {
67        $cached = function_exists( 'get_transient' ) ? get_transient( self::PRICE_EXTENTS_TRANSIENT ) : false;
68        if ( is_array( $cached ) ) {
69            return $cached;
70        }
71
72        $extents = array(
73            'min' => null,
74            'max' => null,
75        );
76
77        if ( function_exists( 'wc_get_product' ) ) {
78            global $wpdb;
79            if ( isset( $wpdb ) && ! empty( $wpdb->wc_product_meta_lookup ) ) {
80                // Same indexed lookup table WC's own slider/widget/Store API hit;
81                // scales linearly with product count (vs. a REGEXP `postmeta` scan).
82                // Joined to `wp_posts` so draft/pending/trashed don't inflate — WC
83                // populates the table on every save, not only on publish.
84                $row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
85                    "SELECT MIN(l.min_price) AS min_price, MAX(l.max_price) AS max_price
86                    FROM {$wpdb->wc_product_meta_lookup} l
87                    INNER JOIN {$wpdb->posts} p ON p.ID = l.product_id
88                    WHERE p.post_status = 'publish'
89                        AND p.post_type IN ( 'product', 'product_variation' )"
90                );
91                if ( $row && null !== $row->min_price && null !== $row->max_price ) {
92                    $extents = array(
93                        'min' => (float) $row->min_price,
94                        'max' => (float) $row->max_price,
95                    );
96                }
97            }
98        }
99
100        if ( function_exists( 'set_transient' ) ) {
101            set_transient( self::PRICE_EXTENTS_TRANSIENT, $extents, self::PRICE_EXTENTS_TTL_SEC );
102        }
103
104        return $extents;
105    }
106}