PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / data-stores / class-wc-product-variable-data-store-cpt.php
woocommerce / includes / data-stores Last commit date
abstract-wc-order-data-store-cpt.php 6 days ago abstract-wc-order-item-type-data-store.php 9 months ago class-wc-coupon-data-store-cpt.php 2 weeks ago class-wc-customer-data-store-session.php 2 weeks ago class-wc-customer-data-store.php 1 month ago class-wc-customer-download-data-store.php 3 years ago class-wc-customer-download-log-data-store.php 2 years ago class-wc-data-store-wp.php 11 months ago class-wc-order-data-store-cpt.php 1 month ago class-wc-order-item-coupon-data-store.php 8 years ago class-wc-order-item-data-store.php 4 months ago class-wc-order-item-fee-data-store.php 8 years ago class-wc-order-item-product-data-store.php 4 years ago class-wc-order-item-shipping-data-store.php 1 year ago class-wc-order-item-tax-data-store.php 7 years ago class-wc-order-refund-data-store-cpt.php 3 months ago class-wc-payment-token-data-store.php 5 months ago class-wc-product-data-store-cpt.php 2 weeks ago class-wc-product-grouped-data-store-cpt.php 5 months ago class-wc-product-variable-data-store-cpt.php 2 weeks ago class-wc-product-variation-data-store-cpt.php 1 month ago class-wc-shipping-zone-data-store.php 7 months ago class-wc-webhook-data-store.php 2 years ago
class-wc-product-variable-data-store-cpt.php
1177 lines
1 <?php
2 /**
3 * File for WC Variable Product Data Store class.
4 *
5 * @package WooCommerce\Classes
6 */
7
8 use Automattic\WooCommerce\Enums\ProductStatus;
9 use Automattic\WooCommerce\Enums\ProductStockStatus;
10 use Automattic\WooCommerce\Enums\TaxDisplayMode;
11 use Automattic\WooCommerce\Internal\Caches\ProductVersionStringInvalidator;
12 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
13 use Automattic\WooCommerce\Utilities\CallbackUtil;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * WC Variable Product Data Store: Stored in CPT.
21 *
22 * @version 3.0.0
23 */
24 class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Product_Variable_Data_Store_Interface {
25
26 /**
27 * Cached & hashed prices array for child variations.
28 *
29 * @var array
30 */
31 protected $prices_array = array();
32
33 /**
34 * Read attributes from post meta.
35 *
36 * @param WC_Product $product Product object.
37 */
38 protected function read_attributes( &$product ) {
39 $product_id = $product->get_id();
40 $meta_attributes = get_post_meta( $product_id, '_product_attributes', true );
41
42 if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
43 $attributes = array();
44 $force_update = false;
45 foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
46 $meta_value = array_merge(
47 array(
48 'name' => '',
49 'value' => '',
50 'position' => 0,
51 'is_visible' => 0,
52 'is_variation' => 0,
53 'is_taxonomy' => 0,
54 ),
55 (array) $meta_attribute_value
56 );
57
58 // Maintain data integrity: WordPress 4.9 changed sanitization functions, and we update the values here so variations function correctly.
59 // Since 10.6.0, we are refactoring the updates into product-level: BC-focused (not all-in on-spot migration), optimized for performance.
60 // Use-case: `_product_attributes` has data populated on WordPress pre-4.8 and containing symbols affected by the breaking changes.
61 if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) {
62 global $wpdb;
63
64 $child_ids = $product->get_children();
65 if ( ! empty( $child_ids ) ) {
66 $products_to_migrate = implode( ', ', $child_ids );
67 $old_slug = 'attribute_' . $meta_attribute_key;
68 $old_meta_rows = $wpdb->get_results(
69 $wpdb->prepare(
70 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
71 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN ( $products_to_migrate )",
72 $old_slug
73 )
74 );
75 if ( $old_meta_rows ) {
76 $new_slug = 'attribute_' . sanitize_title( $meta_value['name'] );
77 foreach ( $old_meta_rows as $old_meta_row ) {
78 update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value );
79 }
80 }
81 }
82
83 $force_update = true;
84 }
85
86 // Check if is a taxonomy attribute.
87 if ( ! empty( $meta_value['is_taxonomy'] ) ) {
88 if ( ! taxonomy_exists( $meta_value['name'] ) ) {
89 continue;
90 }
91 // Performance note: at this stage, the product factory has already primed the caches, so wc_get_object_terms does not present a concern.
92 $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
93 $options = wc_get_object_terms( $product_id, $meta_value['name'], 'term_id' );
94 } else {
95 $id = 0;
96 $options = wc_get_text_attributes( $meta_value['value'] );
97 }
98
99 $attribute = new WC_Product_Attribute();
100 $attribute->set_id( $id );
101 $attribute->set_name( $meta_value['name'] );
102 $attribute->set_options( $options );
103 $attribute->set_position( $meta_value['position'] );
104 $attribute->set_visible( $meta_value['is_visible'] );
105 $attribute->set_variation( $meta_value['is_variation'] );
106
107 /**
108 * Filter product attribute after initialization.
109 *
110 * @since 10.6.0
111 *
112 * @param WC_Product_Attribute $attribute The attribute object.
113 * @param array $meta_value The meta value.
114 * @param WC_Product $product The product object.
115 */
116 $attributes[] = apply_filters( 'woocommerce_product_read_attribute', $attribute, $meta_value, $product );
117 }
118 $product->set_attributes( $attributes );
119
120 if ( $force_update ) {
121 $this->update_attributes( $product, true );
122 }
123 }
124 }
125
126 /**
127 * Read product data.
128 *
129 * @param WC_Product $product Product object.
130 *
131 * @since 3.0.0
132 */
133 protected function read_product_data( &$product ) {
134 // Prime caches to reduce future queries.
135 if ( ! wp_using_ext_object_cache() ) {
136 $product_id = $product->get_id();
137 wp_prime_option_caches(
138 array(
139 // Transients from \WC_Product_Variable_Data_Store_CPT class.
140 '_transient_wc_var_prices_' . $product_id,
141 '_transient_timeout_wc_var_prices_' . $product_id,
142 '_transient_wc_product_children_' . $product_id,
143 '_transient_timeout_wc_product_children_' . $product_id,
144 // Transients from \WC_Product_Variable class.
145 '_transient_wc_child_has_weight_' . $product_id,
146 '_transient_timeout_wc_child_has_weight_' . $product_id,
147 '_transient_wc_child_has_dimensions_' . $product_id,
148 '_transient_timeout_wc_child_has_dimensions_' . $product_id,
149 // Transients from \wc_get_related_products function.
150 '_transient_wc_related_' . $product_id,
151 '_transient_timeout_wc_related_' . $product_id,
152 )
153 );
154 }
155
156 parent::read_product_data( $product );
157
158 // Make sure data which does not apply to variables is unset.
159 $product->set_regular_price( '' );
160 $product->set_sale_price( '' );
161 }
162
163 /**
164 * Loads variation child IDs.
165 *
166 * @param WC_Product $product Product object.
167 * @param bool $force_read True to bypass the transient.
168 *
169 * @return array
170 */
171 public function read_children( &$product, $force_read = false ) {
172 $children_transient_name = 'wc_product_children_' . $product->get_id();
173 $children = get_transient( $children_transient_name );
174 if ( empty( $children ) || ! is_array( $children ) ) {
175 $children = array();
176 }
177
178 $transient_version = WC_Cache_Helper::get_transient_version( 'product' );
179
180 if ( ! $force_read && $children ) {
181 // Validate the children data.
182 if ( ! $this->validate_children_data( $children, $transient_version ) ) {
183 $children = array();
184 $force_read = true;
185 }
186 }
187
188 if ( ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
189 $all_args = array(
190 'post_parent' => $product->get_id(),
191 'post_type' => 'product_variation',
192 'orderby' => array(
193 'menu_order' => 'ASC',
194 'ID' => 'ASC',
195 ),
196 'fields' => 'ids',
197 'post_status' => array( ProductStatus::PUBLISH, ProductStatus::PRIVATE ),
198 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
199 );
200
201 $visible_only_args = $all_args;
202 $visible_only_args['post_status'] = ProductStatus::PUBLISH;
203
204 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
205 $visible_only_args['tax_query'][] = array(
206 'taxonomy' => 'product_visibility',
207 'field' => 'name',
208 'terms' => ProductStockStatus::OUT_OF_STOCK,
209 'operator' => 'NOT IN',
210 );
211 }
212
213 /**
214 * Filters the query arguments used to retrieve variation children of a variable product.
215 *
216 * @since 3.0.0
217 *
218 * @param array $args Query arguments for get_posts().
219 * @param WC_Product $product The variable product object.
220 * @param bool $visible_only True when retrieving only visible variations, false for all variations.
221 */
222 $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) );
223
224 // phpcs:disable WooCommerce.Commenting.CommentHooks
225 $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) );
226
227 // Validate the children data before storing it in the transient.
228 if ( $this->validate_children_data( $children, $transient_version ) ) {
229 set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 );
230 }
231 }
232
233 $children['all'] = wp_parse_id_list( (array) $children['all'] );
234 $children['visible'] = wp_parse_id_list( (array) $children['visible'] );
235
236 return $children;
237 }
238
239 /**
240 * Loads an array of attributes used for variations, as well as their possible values.
241 *
242 * @param WC_Product $product Product object.
243 *
244 * @return array
245 */
246 public function read_variation_attributes( &$product ) {
247 global $wpdb;
248
249 $product_id = $product->get_id();
250 $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . 'product_variation_attributes_' . $product_id;
251 $cached_data = wp_cache_get( $cache_key, 'products' );
252
253 if ( false !== $cached_data ) {
254 return $cached_data;
255 }
256
257 $variation_attributes = array();
258 $attributes = $product->get_attributes();
259
260 if ( ! empty( $attributes ) ) {
261 $child_ids = $product->get_children();
262 $attributes = array_values( array_filter( $attributes, static fn ( $attribute ) => ! empty( $attribute['is_variation'] ) ) );
263
264 $attributes_values = array();
265 if ( ! empty( $child_ids ) && ! empty( $attributes ) ) {
266 // Performance note: for optimal performance, retrieve attribute values using a single SQL query.
267 $attributes_placeholders = implode( ', ', array_fill( 0, count( $attributes ), '%s' ) );
268 $children_placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
269 $prefetch = $wpdb->get_results(
270 $wpdb->prepare(
271 // Performance: DISTINCT + CAST(meta_value AS BINARY(200)) is feasible but forces a temp table — keep array_unique.
272 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
273 "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ( {$children_placeholders} ) AND meta_key IN ( {$attributes_placeholders} )",
274 ...$child_ids,
275 ...array_map( static fn( $attribute ) => wc_variation_attribute_name( $attribute['name'] ), $attributes )
276 )
277 );
278 foreach ( $prefetch as $row ) {
279 // Defensive: normalize non-canonical meta_key casing (e.g. attribute_pa_Size → attribute_pa_size).
280 // sanitize_title over wc_variation_attribute_name as the latter adds 'attribute_' prefix.
281 $attributes_values[ sanitize_title( $row->meta_key ) ][] = $row->meta_value;
282 }
283 }
284
285 foreach ( $attributes as $attribute ) {
286 $values = $attributes_values[ wc_variation_attribute_name( $attribute['name'] ) ] ?? array();
287
288 // Empty value indicates that all options for given attribute are available.
289 if ( empty( $values ) || in_array( null, $values, true ) || in_array( '', $values, true ) ) {
290 $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product_id, $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] );
291 // Get custom attributes (non taxonomy) as defined.
292 } elseif ( ! $attribute['is_taxonomy'] ) {
293 $text_attributes = wc_get_text_attributes( $attribute['value'] );
294 $assigned_text_attributes = $values;
295 $values = array();
296
297 // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
298 if ( version_compare( get_post_meta( $product_id, '_product_version', true ), '2.4.0', '<' ) ) {
299 $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes );
300 foreach ( $text_attributes as $text_attribute ) {
301 if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) {
302 $values[] = $text_attribute;
303 }
304 }
305 } else {
306 foreach ( $text_attributes as $text_attribute ) {
307 if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) {
308 $values[] = $text_attribute;
309 }
310 }
311 }
312 }
313 $variation_attributes[ $attribute['name'] ] = array_unique( $values );
314 }
315 }
316
317 wp_cache_set( $cache_key, $variation_attributes, 'products' );
318
319 return $variation_attributes;
320 }
321
322 /**
323 * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale.
324 *
325 * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter.
326 * This is to ensure modified prices are not cached, unless intended.
327 *
328 * @param WC_Product $product Product object.
329 * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes).
330 *
331 * @return array of prices
332 * @since 3.0.0
333 */
334 public function read_price_data( &$product, $for_display = false ) {
335 /**
336 * If you are here to investigate performance of this method: yes, it is heavy in RAM and CPU usage overall.
337 *
338 * The root cause is that variation object usage is hard-locked by multiple contracts (extension points / filters).
339 * Those contracts are heavily used by popular extensions, rendering most optimization routes irrelevant at scale.
340 *
341 * Optimizations already in place:
342 * - request-level cache ($this->prices_array)
343 * - cross-request cache (transient wc_var_prices_<product_id>; sensitive to product transient invalidation through multiple workflows)
344 * - cache priming (bulk-fetching data from DB) for the product and its variations
345 * - object instance caching (request-level optimization for wc_get_product; applies across Woo core and extensions)
346 *
347 * That leaves two optimization routes:
348 * - rewrite this method, if breaking the contracts is an option (it's not as per WooCommerce v11.1)
349 * - verify transient wc_var_prices_<product_id> invalidation frequency and triggers if it gets critical in later releases
350 */
351 $price_hash = $this->get_price_hash( $product, $for_display );
352 if ( empty( $this->prices_array[ $price_hash ] ) ) {
353 /**
354 * Transient name for storing prices for this product (note: Max transient length is 45)
355 *
356 * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product.
357 */
358 $transient_name = 'wc_var_prices_' . $product->get_id();
359 $transient_version = WC_Cache_Helper::get_transient_version( 'product' );
360 $opposite_price_hash = $this->taxes_influence_price( $product ) ? null : $this->get_price_hash( $product, ! $for_display );
361
362 // If the prices are not valid, reset the transient cache.
363 $transient_cached_prices_array = array_filter( (array) json_decode( (string) get_transient( $transient_name ), true ) );
364 if ( ! $this->validate_prices_data( $transient_cached_prices_array, $transient_version ) ) {
365 $transient_cached_prices_array = array();
366 }
367
368 // If the prices are not stored for this hash, generate them and add to the transient.
369 // Check also the opposite price hash as it may have changed (see get_price_hash).
370 if ( empty( $transient_cached_prices_array[ $price_hash ] ) || ( null !== $opposite_price_hash && empty( $transient_cached_prices_array[ $opposite_price_hash ] ) ) ) {
371 $prices_array = array(
372 'price' => array(),
373 'regular_price' => array(),
374 'sale_price' => array(),
375 );
376
377 $variation_ids = $product->get_visible_children();
378
379 if ( ! empty( $variation_ids ) ) {
380 // Prime caches to reduce future queries.
381 _prime_post_caches( $variation_ids );
382 }
383
384 $tax_display_mode = $for_display ? get_option( 'woocommerce_tax_display_shop' ) : null;
385 $price_decimals = wc_get_price_decimals();
386 foreach ( $variation_ids as $variation_id ) {
387 $variation = wc_get_product( $variation_id );
388
389 if ( $variation ) {
390 /**
391 * Filters the active price for a product variation before it is used in price calculations and caching.
392 *
393 * IMPORTANT: If your filter returns a price that varies by ANY factor (e.g., current user,
394 * time of day, user role, cart contents, etc.), you MUST also hook into the
395 * 'woocommerce_get_variation_prices_hash' filter to include that factor in the cache key.
396 * Otherwise, the wrong price may be cached and displayed to different users or contexts.
397 *
398 * Example: Applying a per-user discount and adapting the price hash info accordingly:
399 *
400 * class My_Custom_Pricing {
401 * public function __construct() {
402 * add_filter( 'woocommerce_variation_prices_price', array( $this, 'apply_user_discount' ), 10, 3 );
403 * add_filter( 'woocommerce_get_variation_prices_hash', array( $this, 'add_user_to_hash' ), 10, 3 );
404 * }
405 *
406 * public function apply_user_discount( $price, $variation, $product ) {
407 * return $price * $this->get_discount_for_user( get_current_user_id() );
408 * }
409 *
410 * public function add_user_to_hash( $price_hash, $product, $for_display ) {
411 * $price_hash[] = get_current_user_id();
412 * return $price_hash;
413 * }
414 * }
415 *
416 * @since 3.0.0
417 *
418 * @param string|float $price The variation's active price.
419 * @param WC_Product $variation The variation product object.
420 * @param WC_Product $product The parent variable product object.
421 */
422 $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
423
424 // Skip empty prices.
425 if ( '' === $price ) {
426 continue;
427 }
428
429 /**
430 * Filters the regular price for a product variation before it is used in price calculations and caching.
431 *
432 * IMPORTANT: see the documentation for the 'woocommerce_variation_prices_price' filter.
433 *
434 * @since 3.0.0
435 *
436 * @param string|float $regular_price The variation's regular price.
437 * @param WC_Product $variation The variation product object.
438 * @param WC_Product $product The parent variable product object.
439 */
440 $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product );
441
442 /**
443 * Filters the sale price for a product variation before it is used in price calculations and caching.
444 *
445 * IMPORTANT: see the documentation for the 'woocommerce_variation_prices_price' filter.
446 *
447 * @since 3.0.0
448 *
449 * @param string|float $sale_price The variation's sale price.
450 * @param WC_Product $variation The variation product object.
451 * @param WC_Product $product The parent variable product object.
452 */
453 $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );
454
455 // If sale price does not equal price, the product is not yet on sale.
456 if ( $sale_price === $regular_price || $sale_price !== $price ) {
457 $sale_price = $regular_price;
458 }
459
460 // If we are getting prices for display, we need to account for taxes.
461 if ( $for_display ) {
462 if ( TaxDisplayMode::INCLUSIVE === $tax_display_mode ) {
463 $price = '' === $price ? '' : wc_get_price_including_tax(
464 $variation,
465 array(
466 'qty' => 1,
467 'price' => $price,
468 )
469 );
470 $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax(
471 $variation,
472 array(
473 'qty' => 1,
474 'price' => $regular_price,
475 )
476 );
477 $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax(
478 $variation,
479 array(
480 'qty' => 1,
481 'price' => $sale_price,
482 )
483 );
484 } else {
485 $price = '' === $price ? '' : wc_get_price_excluding_tax(
486 $variation,
487 array(
488 'qty' => 1,
489 'price' => $price,
490 )
491 );
492 $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax(
493 $variation,
494 array(
495 'qty' => 1,
496 'price' => $regular_price,
497 )
498 );
499 $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax(
500 $variation,
501 array(
502 'qty' => 1,
503 'price' => $sale_price,
504 )
505 );
506 }
507 }
508
509 $prices_array['price'][ $variation_id ] = wc_format_decimal( $price, $price_decimals );
510 $prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, $price_decimals );
511 $prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, $price_decimals );
512
513 if ( has_filter( 'woocommerce_variation_prices_array' ) ) {
514 $original_prices_array = $prices_array;
515
516 /**
517 * Filter the variation prices array before storing in transient cache.
518 *
519 * This filter allows developers to modify the variation prices array for each variation
520 * during the price calculation process. It's called for each variation individually
521 * and can be used to add custom pricing data or modify existing prices.
522 *
523 * @since 3.6.0
524 *
525 * @param array $prices_array The prices array being built. Contains 'price', 'regular_price', and 'sale_price' keys.
526 * @param WC_Product $variation The variation product object.
527 * @param bool $for_display Whether prices are for display (with tax adjustments) or for calculations.
528 */
529 $prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display );
530 if ( null !== $opposite_price_hash ) {
531 // $for_display doesn't affect raw prices here, but a woocommerce_variation_prices_array hook might.
532 // phpcs:ignore WooCommerce.Commenting.CommentHooks
533 $opposite_prices_array = apply_filters( 'woocommerce_variation_prices_array', $original_prices_array, $variation, ! $for_display );
534 if ( $opposite_prices_array !== $prices_array ) {
535 $opposite_price_hash = null;
536 }
537 }
538 }
539 }
540 }
541
542 // Add all pricing data to the transient array.
543 foreach ( $prices_array as $key => $values ) {
544 $transient_cached_prices_array[ $price_hash ][ $key ] = $values;
545 if ( null !== $opposite_price_hash ) {
546 $transient_cached_prices_array[ $opposite_price_hash ][ $key ] = $values;
547 }
548 }
549
550 // Validate the prices data before storing it in the transient.
551 if ( $this->validate_prices_data( $transient_cached_prices_array, $transient_version ) ) {
552 set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 );
553 }
554 }
555
556 /**
557 * Filters the variation prices array for a variable product.
558 *
559 * This filter gives plugins one last chance to modify the variation prices array which has been
560 * generated and will be stored locally to the class. This value may differ from the transient cache.
561 * It is filtered once before storing locally.
562 *
563 * @since 3.0.0
564 *
565 * @param array $prices_array {
566 * Associative array of variation prices indexed by variation ID.
567 *
568 * @type array $price Array of active prices (variation_id => price).
569 * @type array $regular_price Array of regular prices (variation_id => price).
570 * @type array $sale_price Array of sale prices (variation_id => price).
571 * }
572 * @param WC_Product $product The variable product object.
573 * @param bool $for_display Whether prices are being retrieved for display.
574 */
575 $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display );
576 if ( $this->prices_array[ $price_hash ] !== $transient_cached_prices_array[ $price_hash ] && ! $this->validate_prices_data( array( $price_hash => $this->prices_array[ $price_hash ] ), $transient_version ) ) {
577 wc_doing_it_wrong(
578 __METHOD__,
579 __( '`woocommerce_variation_prices` returned an unsupported data format. The value is currently used as-is but will be ignored in a future release. Ensure your callback returns the expected array structure.', 'woocommerce' ),
580 '11.1'
581 );
582 }
583
584 if ( null !== $opposite_price_hash && $opposite_price_hash !== $price_hash ) {
585 $this->prices_array[ $opposite_price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $opposite_price_hash ], $product, ! $for_display );
586 if ( $this->prices_array[ $opposite_price_hash ] !== $transient_cached_prices_array[ $opposite_price_hash ] && ! $this->validate_prices_data( array( $opposite_price_hash => $this->prices_array[ $opposite_price_hash ] ), $transient_version ) ) {
587 wc_doing_it_wrong(
588 __METHOD__,
589 __( '`woocommerce_variation_prices` returned an unsupported data format. The value is currently used as-is but will be ignored in a future release. Ensure your callback returns the expected array structure.', 'woocommerce' ),
590 '11.1'
591 );
592 }
593 }
594 }
595 return $this->prices_array[ $price_hash ];
596 }
597
598 /**
599 * Check if the prices for a product will be different with or without taxes.
600 *
601 * @param WC_Product $product Product to check.
602 * @return bool True if the prices will be different with or without taxes.
603 *
604 * @since 10.4.0
605 */
606 protected function taxes_influence_price( $product ): bool {
607 if ( ! $product->is_taxable() ) {
608 $taxes_influence_price = false;
609 } elseif ( empty( WC_Tax::get_rates( $product->get_tax_class() ) ) ) {
610 $taxes_influence_price = false;
611 } else {
612 // Taxes influence the price regardless of VAT exempt status. Even when a
613 // customer is VAT exempt, the displayed prices differ from non-exempt
614 // prices, so they need separate cache entries and the opposite_price_hash
615 // optimization should not apply. Returning false here was causing cached
616 // non-exempt prices to be served to VAT exempt customers.
617 $taxes_influence_price = true;
618 }
619
620 /**
621 * Filters whether taxes influence the displayed price of a variable product.
622 *
623 * Return `true` from this filter to force separate cache entries for the two
624 * variants. This is needed when an extension produces displayed prices that
625 * differ from raw prices independently of the standard tax calculation, for
626 * example when computing per-country prices for locations that have no
627 * configured tax rates.
628 *
629 * @param bool $taxes_influence_price Default decision based on product taxability and configured tax rates.
630 * @param WC_Product $product The variable product being evaluated.
631 *
632 * @since 10.9.0
633 */
634 return (bool) apply_filters( 'woocommerce_variable_product_taxes_influence_price', $taxes_influence_price, $product );
635 }
636
637 /**
638 * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters.
639 * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique.
640 *
641 * @param WC_Product $product Product object.
642 * @param bool $for_display If taxes should be calculated or not.
643 *
644 * @since 3.0.0
645 * @return string
646 */
647 protected function get_price_hash( &$product, $for_display = false ) {
648 $price_hash = array( false );
649
650 if ( $for_display && wc_tax_enabled() ) {
651 $price_hash = array(
652 get_option( 'woocommerce_tax_display_shop', TaxDisplayMode::EXCLUSIVE ),
653 WC_Tax::get_rates(),
654 ! empty( WC()->customer ) && WC()->customer->is_vat_exempt(),
655 );
656 }
657
658 $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' );
659
660 /**
661 * Filters whether to use the legacy callback serialization algorithm.
662 *
663 * By default, WooCommerce will use the legacy algorithm to get the callback signatures
664 * for variation price hash calculation. That algorithm includes the callback array as it
665 * comes from $wp_filter in the hashed data, which is then JSON encoded. For callbacks that
666 * are class methods, JSON encoding captures the object's PUBLIC property values only;
667 * private and protected properties are not captured. Note that dynamically created
668 * properties are public, and that a class implementing JsonSerializable controls what is
669 * captured through its jsonSerialize() method. A change in any captured value will change
670 * the price hash, even if it does not affect the price calculation.
671 *
672 * This filter allows using CallbackUtil instead, which generates a more stable signature
673 * that does not depend on the internal state of objects, but only on the method names and
674 * class names. This results in a more consistent and reliable price hash, reducing unnecessary
675 * cache misses; but can cause compatibility issues with plugins that rely on the legacy behavior.
676 *
677 * IMPORTANT: see also the documentation for the 'woocommerce_variation_prices_price' filter.
678 *
679 * @since 10.5.0
680 *
681 * @param bool $use_legacy True to use the legacy algorithm (default), false to use CallbackUtil
682 * @param WC_Product $product The product object.
683 * @param bool $for_display If taxes should be calculated or not.
684 */
685 $use_legacy_algorithm = apply_filters( 'woocommerce_use_legacy_get_variations_price_hash', true, $product, $for_display );
686
687 if ( $use_legacy_algorithm ) {
688 global $wp_filter;
689
690 foreach ( $filter_names as $filter_name ) {
691 if ( ! empty( $wp_filter[ $filter_name ] ) ) {
692 $price_hash[ $filter_name ] = array();
693
694 foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) {
695 $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
696 }
697 }
698 }
699 } else {
700 foreach ( $filter_names as $filter_name ) {
701 $signatures = CallbackUtil::get_hook_callback_signatures( $filter_name );
702 if ( ! empty( $signatures ) ) {
703 $price_hash[ $filter_name ] = $signatures;
704 }
705 }
706 }
707
708 /**
709 * Filters the hash used for caching variation prices.
710 *
711 * IMPORTANT: see the documentation for the 'woocommerce_variation_prices_price' filter.
712 *
713 * @since 2.5.0
714 *
715 * @param array $price_hash Array of factors used to generate the cache key hash.
716 * @param WC_Product $product The variable product object.
717 * @param bool $for_display Whether prices are for display (with tax adjustments) or calculations.
718 */
719 $price_hash = apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display );
720
721 return md5( wp_json_encode( $price_hash ) );
722 }
723
724 /**
725 * Does a child have a weight set?
726 *
727 * @param WC_Product $product Product object.
728 *
729 * @since 3.0.0
730 * @return boolean
731 */
732 public function child_has_weight( $product ) {
733 global $wpdb;
734
735 $has_weight = false;
736 $child_ids = $product->get_visible_children();
737 if ( ! empty( $child_ids ) ) {
738 $placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
739 $has_weight = (bool) $wpdb->get_var(
740 $wpdb->prepare(
741 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
742 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN ( {$placeholders} ) LIMIT 1",
743 $child_ids
744 )
745 );
746 }
747
748 return $has_weight;
749 }
750
751 /**
752 * Does a child have dimensions set?
753 *
754 * @param WC_Product $product Product object.
755 *
756 * @since 3.0.0
757 * @return boolean
758 */
759 public function child_has_dimensions( $product ) {
760 global $wpdb;
761
762 $has_dimensions = false;
763 $child_ids = $product->get_visible_children();
764 if ( ! empty( $child_ids ) ) {
765 $placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
766 $has_dimensions = (bool) $wpdb->get_var(
767 $wpdb->prepare(
768 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
769 "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN ( {$placeholders} ) LIMIT 1",
770 $child_ids
771 )
772 );
773 }
774
775 return $has_dimensions;
776 }
777
778 /**
779 * Is a child in stock?
780 *
781 * @param WC_Product $product Product object.
782 *
783 * @since 3.0.0
784 * @return boolean
785 */
786 public function child_is_in_stock( $product ) {
787 return $this->child_has_stock_status( $product, ProductStockStatus::IN_STOCK );
788 }
789
790 /**
791 * Does a child have a stock status?
792 *
793 * @param WC_Product $product Product object.
794 * @param string $status 'instock', 'outofstock', or 'onbackorder'.
795 *
796 * @since 3.3.0
797 * @return boolean
798 */
799 public function child_has_stock_status( $product, $status ) {
800 global $wpdb;
801
802 $has_matches = false;
803 $child_ids = $product->get_children();
804 if ( ! empty( $child_ids ) ) {
805 $placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
806 if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) {
807 $query = "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN ( {$placeholders} ) LIMIT 1";
808 } else {
809 $query = "SELECT product_id FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN ( {$placeholders} ) LIMIT 1";
810 }
811 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
812 $has_matches = (bool) $wpdb->get_var( $wpdb->prepare( $query, $status, ...$child_ids ) );
813 }
814
815 return $has_matches;
816 }
817
818 /**
819 * Syncs all variation names if the parent name is changed.
820 *
821 * @param WC_Product $product Product object.
822 * @param string $previous_name Variation previous name.
823 * @param string $new_name Variation new name.
824 *
825 * @since 3.0.0
826 */
827 public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) {
828 if ( $new_name !== $previous_name ) {
829 global $wpdb;
830
831 $product_id = $product->get_id();
832 $wpdb->query(
833 $wpdb->prepare(
834 "UPDATE {$wpdb->posts}
835 SET post_title = REPLACE( post_title, %s, %s )
836 WHERE post_type = 'product_variation'
837 AND post_parent = %d",
838 $previous_name ? $previous_name : 'AUTO-DRAFT',
839 $new_name,
840 $product_id
841 )
842 );
843
844 $invalidator = wc_get_container()->get( ProductVersionStringInvalidator::class );
845 foreach ( $product->get_children() as $child_id ) {
846 clean_post_cache( $child_id );
847 $invalidator->invalidate( $child_id );
848 }
849 $invalidator->invalidate( $product_id );
850 }
851 }
852
853 /**
854 * Stock managed at the parent level - update children being managed by this product.
855 * This sync function syncs downwards (from parent to child) when the variable product is saved.
856 *
857 * @param WC_Product $product Product object.
858 *
859 * @since 3.0.0
860 */
861 public function sync_managed_variation_stock_status( &$product ) {
862 global $wpdb;
863
864 if ( $product->get_manage_stock() ) {
865 $child_ids = $product->get_children();
866 if ( ! empty( $child_ids ) ) {
867 $status = $product->get_stock_status();
868 $placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
869 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
870 $children_to_sync = $wpdb->get_col(
871 $wpdb->prepare(
872 "SELECT post_id
873 FROM {$wpdb->postmeta}
874 WHERE meta_key = '_stock_status'
875 AND meta_value != %s
876 AND post_id IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN ( {$placeholders} ) )",
877 $status,
878 ...$child_ids
879 )
880 );
881 $children_to_backfill = $wpdb->get_col(
882 $wpdb->prepare(
883 "SELECT post_id
884 FROM {$wpdb->postmeta}
885 WHERE meta_key = '_manage_stock'
886 AND meta_value != 'yes'
887 AND post_id IN ( {$placeholders} )
888 AND post_id NOT IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value IS NOT NULL AND post_id IN ( {$placeholders} ) )",
889 ...$child_ids,
890 ...$child_ids
891 )
892 );
893 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
894
895 $children_to_sync = array_unique( array_merge( $children_to_sync, $children_to_backfill ) );
896 if ( ! empty( $children_to_sync ) ) {
897 $invalidator = wc_get_container()->get( ProductVersionStringInvalidator::class );
898
899 foreach ( $children_to_sync as $child_id ) {
900 update_post_meta( $child_id, '_stock_status', $status );
901 $this->update_lookup_table( $child_id, 'wc_product_meta_lookup' );
902 $invalidator->invalidate( $child_id );
903 }
904
905 $children = $this->read_children( $product, true );
906 $product->set_children( $children['all'] );
907 $product->set_visible_children( $children['visible'] );
908 $invalidator->invalidate( $product->get_id() );
909 }
910 }
911 }
912 }
913
914 /**
915 * Sync variable product prices with children.
916 *
917 * @param WC_Product $product Product object.
918 *
919 * @since 3.0.0
920 */
921 public function sync_price( &$product ) {
922 global $wpdb;
923
924 $prices = array();
925 $child_ids = $product->get_visible_children();
926 if ( ! empty( $child_ids ) ) {
927 $placeholders = implode( ', ', array_fill( 0, count( $child_ids ), '%d' ) );
928 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
929 $prices = $wpdb->get_col(
930 $wpdb->prepare(
931 "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_price' AND post_id IN ( {$placeholders} )",
932 $child_ids
933 )
934 );
935 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
936 }
937
938 $product_id = $product->get_id();
939
940 delete_post_meta( $product_id, '_price' );
941 delete_post_meta( $product_id, '_sale_price' );
942 delete_post_meta( $product_id, '_regular_price' );
943
944 if ( $prices ) {
945 sort( $prices, SORT_NUMERIC );
946 // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner.
947 foreach ( $prices as $price ) {
948 if ( is_null( $price ) || '' === $price ) {
949 continue;
950 }
951 add_post_meta( $product_id, '_price', $price, false );
952 }
953 }
954
955 $this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );
956
957 /**
958 * Fire an action for this direct update so it can be detected by other code.
959 *
960 * @since 3.6
961 * @param int $product_id Product ID that was updated directly.
962 */
963 do_action( 'woocommerce_updated_product_price', $product_id );
964 }
965
966 /**
967 * Sync variable product stock status with children.
968 * Change does not persist unless saved by caller.
969 *
970 * @param WC_Product_Variable $product Product object.
971 *
972 * @since 3.0.0
973 */
974 public function sync_stock_status( &$product ) {
975 // Performance note: direct DB fetch would be faster, but child_is_in_stock/child_is_on_backorder are overridable — keep delegation.
976 if ( $product->child_is_in_stock() ) {
977 $product->set_stock_status( ProductStockStatus::IN_STOCK );
978 } elseif ( $product->child_is_on_backorder() ) {
979 $product->set_stock_status( ProductStockStatus::ON_BACKORDER );
980 } else {
981 $product->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
982 }
983 }
984
985 /**
986 * Delete variations of a product.
987 *
988 * @param int $product_id Product ID.
989 * @param bool $force_delete False to trash.
990 *
991 * @since 3.0.0
992 */
993 public function delete_variations( $product_id, $force_delete = false ) {
994 if ( ! is_numeric( $product_id ) || 0 >= $product_id ) {
995 return;
996 }
997
998 $variation_ids = wp_parse_id_list(
999 get_posts(
1000 array(
1001 'post_parent' => $product_id,
1002 'post_type' => 'product_variation',
1003 'fields' => 'ids',
1004 'post_status' => array( 'any', ProductStatus::TRASH, ProductStatus::AUTO_DRAFT ),
1005 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
1006 )
1007 )
1008 );
1009
1010 if ( ! empty( $variation_ids ) ) {
1011 foreach ( $variation_ids as $variation_id ) {
1012 if ( $force_delete ) {
1013 /**
1014 * Fires before a product variation is permanently deleted.
1015 *
1016 * @since 3.0.0
1017 *
1018 * @param int $variation_id The variation ID being deleted.
1019 */
1020 do_action( 'woocommerce_before_delete_product_variation', $variation_id );
1021 wp_delete_post( $variation_id, true );
1022
1023 /**
1024 * Fires after a product variation has been permanently deleted.
1025 *
1026 * @since 3.0.0
1027 *
1028 * @param int $variation_id The variation ID that was deleted.
1029 */
1030 do_action( 'woocommerce_delete_product_variation', $variation_id );
1031 } else {
1032 wp_trash_post( $variation_id );
1033
1034 /**
1035 * Fires after a product variation has been trashed.
1036 *
1037 * @since 3.0.0
1038 *
1039 * @param int $variation_id The variation ID that was trashed.
1040 */
1041 do_action( 'woocommerce_trash_product_variation', $variation_id );
1042 }
1043 }
1044 }
1045
1046 wc_get_container()->get( ProductUtil::class )->delete_product_specific_transients_for_products( array( $product_id ) );
1047 }
1048
1049 /**
1050 * Untrash variations.
1051 *
1052 * @param int $product_id Product ID.
1053 */
1054 public function untrash_variations( $product_id ) {
1055 $variation_ids = wp_parse_id_list(
1056 get_posts(
1057 array(
1058 'post_parent' => $product_id,
1059 'post_type' => 'product_variation',
1060 'fields' => 'ids',
1061 'post_status' => 'trash',
1062 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts
1063 )
1064 )
1065 );
1066
1067 if ( ! empty( $variation_ids ) ) {
1068 foreach ( $variation_ids as $variation_id ) {
1069 wp_untrash_post( $variation_id );
1070 }
1071 }
1072
1073 wc_get_container()->get( ProductUtil::class )->delete_product_specific_transients_for_products( array( $product_id ) );
1074 }
1075
1076 /**
1077 * Validate the children data by checking the structure and type of the data.
1078 *
1079 * @param array $children The children data.
1080 * @param string $deprecated Was the current transient version, unused since 10.3.0.
1081 * @return bool True if valid, false otherwise.
1082 */
1083 protected function validate_children_data( $children, $deprecated ) {
1084 if ( ! is_array( $children ) ) {
1085 return false;
1086 }
1087
1088 // Basic structure checks.
1089 if ( empty( $children['all'] ) || ! isset( $children['visible'] ) ) {
1090 return false;
1091 }
1092
1093 if ( ! is_array( $children['all'] ) || ! is_array( $children['visible'] ) ) {
1094 return false;
1095 }
1096
1097 foreach ( $children['all'] as $id ) {
1098 if ( ! is_numeric( $id ) ) {
1099 return false;
1100 }
1101 }
1102
1103 foreach ( $children['visible'] as $id ) {
1104 if ( ! is_numeric( $id ) ) {
1105 return false;
1106 }
1107 }
1108
1109 return true;
1110 }
1111
1112 /**
1113 * Validate the prices data by checking the structure and type of the data.
1114 *
1115 * @param array $prices_array The prices data.
1116 * @param string $deprecated Was the current transient version, unused since 10.3.0.
1117 * @return bool True if valid, false otherwise.
1118 */
1119 protected function validate_prices_data( $prices_array, $deprecated ) {
1120 if ( ! is_array( $prices_array ) ) {
1121 return false;
1122 }
1123
1124 // Fail if array is empty - we want to rebuild in this case.
1125 if ( empty( $prices_array ) ) {
1126 return false;
1127 }
1128
1129 $price_data_is_empty = true;
1130
1131 foreach ( $prices_array as $price_data ) {
1132 if ( ! is_array( $price_data ) ) {
1133 return false;
1134 }
1135
1136 $required_types = array( 'price', 'regular_price', 'sale_price' );
1137
1138 foreach ( $required_types as $type ) {
1139 // If all 'price' fields are empty, we want to track that so we can rebuild the data.
1140 if ( 'price' === $type && ! empty( $price_data[ $type ] ) && $price_data_is_empty ) {
1141 $price_data_is_empty = false;
1142 }
1143
1144 if ( ! isset( $price_data[ $type ] ) || ! is_array( $price_data[ $type ] ) ) {
1145 return false;
1146 }
1147 }
1148
1149 $variation_ids = array_keys( $price_data['price'] );
1150
1151 foreach ( $variation_ids as $variation_id ) {
1152 if ( ! is_numeric( $variation_id ) ) {
1153 return false;
1154 }
1155
1156 foreach ( $required_types as $type ) {
1157 if ( ! array_key_exists( $variation_id, $price_data[ $type ] ) ) {
1158 return false;
1159 }
1160
1161 $type_price = $price_data[ $type ][ $variation_id ];
1162 if ( ! is_numeric( $type_price ) && '' !== $type_price ) {
1163 return false;
1164 }
1165 }
1166 }
1167 }
1168
1169 // If price is empty, we want to rebuild the data.
1170 if ( $price_data_is_empty ) {
1171 return false;
1172 }
1173
1174 return true;
1175 }
1176 }
1177