These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * WC_Product_Data_Store_CPT class file. |
||
4 | * |
||
5 | * @package WooCommerce/Classes |
||
6 | */ |
||
7 | |||
8 | if ( ! defined( 'ABSPATH' ) ) { |
||
9 | exit; |
||
10 | } |
||
11 | |||
12 | /** |
||
13 | * WC Product Data Store: Stored in CPT. |
||
14 | * |
||
15 | * @version 3.0.0 |
||
16 | */ |
||
17 | class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface { |
||
18 | |||
19 | /** |
||
20 | * Data stored in meta keys, but not considered "meta". |
||
21 | * |
||
22 | * @since 3.0.0 |
||
23 | * @var array |
||
24 | */ |
||
25 | protected $internal_meta_keys = array( |
||
26 | '_visibility', |
||
27 | '_sku', |
||
28 | '_price', |
||
29 | '_regular_price', |
||
30 | '_sale_price', |
||
31 | '_sale_price_dates_from', |
||
32 | '_sale_price_dates_to', |
||
33 | 'total_sales', |
||
34 | '_tax_status', |
||
35 | '_tax_class', |
||
36 | '_manage_stock', |
||
37 | '_stock', |
||
38 | '_stock_status', |
||
39 | '_backorders', |
||
40 | '_low_stock_amount', |
||
41 | '_sold_individually', |
||
42 | '_weight', |
||
43 | '_length', |
||
44 | '_width', |
||
45 | '_height', |
||
46 | '_upsell_ids', |
||
47 | '_crosssell_ids', |
||
48 | '_purchase_note', |
||
49 | '_default_attributes', |
||
50 | '_product_attributes', |
||
51 | '_virtual', |
||
52 | '_downloadable', |
||
53 | '_download_limit', |
||
54 | '_download_expiry', |
||
55 | '_featured', |
||
56 | '_downloadable_files', |
||
57 | '_wc_rating_count', |
||
58 | '_wc_average_rating', |
||
59 | '_wc_review_count', |
||
60 | '_variation_description', |
||
61 | '_thumbnail_id', |
||
62 | '_file_paths', |
||
63 | '_product_image_gallery', |
||
64 | '_product_version', |
||
65 | '_wp_old_slug', |
||
66 | '_edit_last', |
||
67 | '_edit_lock', |
||
68 | ); |
||
69 | |||
70 | /** |
||
71 | * If we have already saved our extra data, don't do automatic / default handling. |
||
72 | * |
||
73 | * @var bool |
||
74 | */ |
||
75 | protected $extra_data_saved = false; |
||
76 | |||
77 | /** |
||
78 | * Stores updated props. |
||
79 | * |
||
80 | * @var array |
||
81 | */ |
||
82 | protected $updated_props = array(); |
||
83 | |||
84 | /* |
||
85 | |-------------------------------------------------------------------------- |
||
86 | | CRUD Methods |
||
87 | |-------------------------------------------------------------------------- |
||
88 | */ |
||
89 | |||
90 | /** |
||
91 | * Method to create a new product in the database. |
||
92 | * |
||
93 | * @param WC_Product $product Product object. |
||
94 | */ |
||
95 | 381 | public function create( &$product ) { |
|
96 | 381 | if ( ! $product->get_date_created( 'edit' ) ) { |
|
97 | 381 | $product->set_date_created( current_time( 'timestamp', true ) ); |
|
98 | } |
||
99 | |||
100 | 381 | $id = wp_insert_post( |
|
101 | 381 | apply_filters( |
|
102 | 381 | 'woocommerce_new_product_data', |
|
103 | 381 | array( |
|
104 | 381 | 'post_type' => 'product', |
|
105 | 381 | 'post_status' => $product->get_status() ? $product->get_status() : 'publish', |
|
106 | 381 | 'post_author' => get_current_user_id(), |
|
107 | 381 | 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), |
|
108 | 381 | 'post_content' => $product->get_description(), |
|
109 | 381 | 'post_excerpt' => $product->get_short_description(), |
|
110 | 381 | 'post_parent' => $product->get_parent_id(), |
|
111 | 381 | 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', |
|
112 | 381 | 'ping_status' => 'closed', |
|
113 | 381 | 'menu_order' => $product->get_menu_order(), |
|
114 | 381 | 'post_password' => $product->get_post_password( 'edit' ), |
|
115 | 381 | 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), |
|
116 | 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), |
||
117 | 381 | 'post_name' => $product->get_slug( 'edit' ), |
|
118 | ) |
||
119 | ), |
||
120 | 381 | true |
|
121 | 381 | ); |
|
122 | |||
123 | 381 | View Code Duplication | if ( $id && ! is_wp_error( $id ) ) { |
124 | 381 | $product->set_id( $id ); |
|
125 | 381 | ||
126 | 381 | $this->update_post_meta( $product, true ); |
|
127 | 381 | $this->update_terms( $product, true ); |
|
128 | 381 | $this->update_visibility( $product, true ); |
|
129 | $this->update_attributes( $product, true ); |
||
130 | 381 | $this->update_version_and_type( $product ); |
|
131 | 381 | $this->handle_updated_props( $product ); |
|
132 | $this->clear_caches( $product ); |
||
133 | 381 | ||
134 | $product->save_meta_data(); |
||
135 | 381 | $product->apply_changes(); |
|
136 | |||
137 | do_action( 'woocommerce_new_product', $id ); |
||
138 | } |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Method to read a product from the database. |
||
143 | * |
||
144 | * @param WC_Product $product Product object. |
||
145 | 383 | * @throws Exception If invalid product. |
|
146 | 383 | */ |
|
147 | 383 | public function read( &$product ) { |
|
148 | $product->set_defaults(); |
||
149 | 383 | $post_object = get_post( $product->get_id() ); |
|
150 | 6 | ||
151 | View Code Duplication | if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) { |
|
152 | throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); |
||
153 | 381 | } |
|
154 | |||
155 | 381 | $product->set_props( |
|
156 | 381 | array( |
|
157 | 381 | 'name' => $post_object->post_title, |
|
158 | 381 | 'slug' => $post_object->post_name, |
|
159 | 381 | 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, |
|
160 | 381 | 'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null, |
|
161 | 381 | 'status' => $post_object->post_status, |
|
162 | 381 | 'description' => $post_object->post_content, |
|
163 | 381 | 'short_description' => $post_object->post_excerpt, |
|
164 | 381 | 'parent_id' => $post_object->post_parent, |
|
165 | 'menu_order' => $post_object->menu_order, |
||
166 | 'post_password' => $post_object->post_password, |
||
167 | 'reviews_allowed' => 'open' === $post_object->comment_status, |
||
168 | 381 | ) |
|
169 | 381 | ); |
|
170 | 381 | ||
171 | 381 | $this->read_attributes( $product ); |
|
172 | 381 | $this->read_downloads( $product ); |
|
173 | 381 | $this->read_visibility( $product ); |
|
174 | $this->read_product_data( $product ); |
||
175 | $this->read_extra_data( $product ); |
||
176 | $product->set_object_read( true ); |
||
177 | } |
||
178 | |||
179 | /** |
||
180 | * Method to update a product in the database. |
||
181 | 85 | * |
|
182 | 85 | * @param WC_Product $product Product object. |
|
183 | 85 | */ |
|
184 | public function update( &$product ) { |
||
185 | $product->save_meta_data(); |
||
186 | 85 | $changes = $product->get_changes(); |
|
187 | |||
188 | 13 | // Only update the post when the post data changes. |
|
189 | 13 | if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { |
|
190 | 13 | $post_data = array( |
|
191 | 13 | 'post_content' => $product->get_description( 'edit' ), |
|
192 | 13 | 'post_excerpt' => $product->get_short_description( 'edit' ), |
|
193 | 13 | 'post_title' => $product->get_name( 'edit' ), |
|
194 | 13 | 'post_parent' => $product->get_parent_id( 'edit' ), |
|
195 | 13 | 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', |
|
196 | 13 | 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', |
|
197 | 'menu_order' => $product->get_menu_order( 'edit' ), |
||
198 | 13 | 'post_password' => $product->get_post_password( 'edit' ), |
|
199 | 13 | 'post_name' => $product->get_slug( 'edit' ), |
|
200 | 13 | 'post_type' => 'product', |
|
201 | ); |
||
202 | 13 | if ( $product->get_date_created( 'edit' ) ) { |
|
203 | 2 | $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); |
|
204 | 2 | $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); |
|
205 | } |
||
206 | 11 | if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { |
|
207 | 11 | $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); |
|
208 | $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); |
||
209 | } else { |
||
210 | $post_data['post_modified'] = current_time( 'mysql' ); |
||
211 | $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * When updating this object, to prevent infinite loops, use $wpdb |
||
216 | * to update data, since wp_update_post spawns more calls to the |
||
217 | * save_post action. |
||
218 | 13 | * |
|
219 | * This ensures hooks are fired by either WP itself (admin screen save), |
||
220 | * or an update purely from CRUD. |
||
221 | */ |
||
222 | 13 | View Code Duplication | if ( doing_action( 'save_post' ) ) { |
223 | $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); |
||
224 | 13 | clean_post_cache( $product->get_id() ); |
|
225 | } else { |
||
226 | wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); |
||
227 | 78 | } |
|
228 | 78 | $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. |
|
229 | |||
230 | 78 | View Code Duplication | } else { // Only update post modified time to record this save event. |
231 | 78 | $GLOBALS['wpdb']->update( |
|
232 | $GLOBALS['wpdb']->posts, |
||
233 | array( |
||
234 | 78 | 'post_modified' => current_time( 'mysql' ), |
|
235 | 'post_modified_gmt' => current_time( 'mysql', 1 ), |
||
236 | ), |
||
237 | 78 | array( |
|
238 | 'ID' => $product->get_id(), |
||
239 | ) |
||
240 | 85 | ); |
|
241 | 85 | clean_post_cache( $product->get_id() ); |
|
242 | 85 | } |
|
243 | 85 | ||
244 | 85 | $this->update_post_meta( $product ); |
|
245 | 85 | $this->update_terms( $product ); |
|
246 | $this->update_visibility( $product ); |
||
247 | 85 | $this->update_attributes( $product ); |
|
248 | $this->update_version_and_type( $product ); |
||
249 | 85 | $this->handle_updated_props( $product ); |
|
250 | $this->clear_caches( $product ); |
||
251 | 85 | ||
252 | $product->apply_changes(); |
||
253 | |||
254 | do_action( 'woocommerce_update_product', $product->get_id() ); |
||
255 | } |
||
256 | |||
257 | /** |
||
258 | * Method to delete a product from the database. |
||
259 | * |
||
260 | 27 | * @param WC_Product $product Product object. |
|
261 | 27 | * @param array $args Array of args to pass to the delete method. |
|
262 | 27 | */ |
|
263 | public function delete( &$product, $args = array() ) { |
||
264 | 27 | $id = $product->get_id(); |
|
265 | 27 | $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; |
|
266 | 27 | ||
267 | $args = wp_parse_args( |
||
268 | $args, |
||
269 | array( |
||
270 | 27 | 'force_delete' => false, |
|
271 | ) |
||
272 | ); |
||
273 | |||
274 | 27 | if ( ! $id ) { |
|
275 | 25 | return; |
|
276 | 25 | } |
|
277 | 25 | ||
278 | 25 | if ( $args['force_delete'] ) { |
|
279 | do_action( 'woocommerce_before_delete_' . $post_type, $id ); |
||
280 | 2 | wp_delete_post( $id ); |
|
281 | 2 | $product->set_id( 0 ); |
|
282 | 2 | do_action( 'woocommerce_delete_' . $post_type, $id ); |
|
283 | } else { |
||
284 | wp_trash_post( $id ); |
||
285 | $product->set_status( 'trash' ); |
||
286 | do_action( 'woocommerce_trash_' . $post_type, $id ); |
||
287 | } |
||
288 | } |
||
289 | |||
290 | /* |
||
291 | |-------------------------------------------------------------------------- |
||
292 | | Additional Methods |
||
293 | |-------------------------------------------------------------------------- |
||
294 | */ |
||
295 | |||
296 | /** |
||
297 | * Read product data. Can be overridden by child classes to load other props. |
||
298 | 381 | * |
|
299 | 381 | * @param WC_Product $product Product object. |
|
300 | 381 | * @since 3.0.0 |
|
301 | 381 | */ |
|
302 | 381 | protected function read_product_data( &$product ) { |
|
303 | $id = $product->get_id(); |
||
304 | 381 | $post_meta_values = get_post_meta( $id ); |
|
305 | 2 | $meta_key_to_props = array( |
|
306 | '_sku' => 'sku', |
||
307 | 381 | '_regular_price' => 'regular_price', |
|
308 | '_sale_price' => 'sale_price', |
||
309 | '_price' => 'price', |
||
310 | 381 | '_sale_price_dates_from' => 'date_on_sale_from', |
|
311 | 2 | '_sale_price_dates_to' => 'date_on_sale_to', |
|
312 | 'total_sales' => 'total_sales', |
||
313 | 381 | '_tax_status' => 'tax_status', |
|
314 | '_tax_class' => 'tax_class', |
||
315 | '_manage_stock' => 'manage_stock', |
||
316 | 381 | '_backorders' => 'backorders', |
|
317 | 2 | '_low_stock_amount' => 'low_stock_amount', |
|
318 | '_sold_individually' => 'sold_individually', |
||
319 | 381 | '_weight' => 'weight', |
|
320 | '_length' => 'length', |
||
321 | '_width' => 'width', |
||
322 | 381 | '_height' => 'height', |
|
323 | '_upsell_ids' => 'upsell_ids', |
||
324 | 381 | '_crosssell_ids' => 'cross_sell_ids', |
|
325 | 381 | '_purchase_note' => 'purchase_note', |
|
326 | 381 | '_default_attributes' => 'default_attributes', |
|
327 | 381 | '_virtual' => 'virtual', |
|
328 | 381 | '_downloadable' => 'downloadable', |
|
329 | 381 | '_download_limit' => 'download_limit', |
|
330 | 381 | '_download_expiry' => 'download_expiry', |
|
331 | 381 | '_thumbnail_id' => 'image_id', |
|
332 | 381 | '_stock' => 'stock_quantity', |
|
333 | 381 | '_stock_status' => 'stock_status', |
|
334 | 381 | '_wc_average_rating' => 'average_rating', |
|
335 | 381 | '_wc_rating_count' => 'rating_counts', |
|
336 | 381 | '_wc_review_count' => 'review_count', |
|
337 | 381 | '_product_image_gallery' => 'gallery_image_ids', |
|
338 | 381 | ); |
|
339 | 381 | ||
340 | 381 | $set_props = array(); |
|
341 | 381 | ||
342 | 381 | foreach ( $meta_key_to_props as $meta_key => $prop ) { |
|
343 | 381 | $meta_value = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : ''; |
|
344 | 381 | $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values. |
|
345 | 381 | } |
|
346 | 381 | ||
347 | 381 | $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); |
|
348 | 381 | $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); |
|
349 | 381 | $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); |
|
350 | 381 | $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); |
|
351 | 381 | ||
352 | 381 | if ( '' === $set_props['review_count'] ) { |
|
353 | 381 | unset( $set_props['review_count'] ); |
|
354 | 381 | WC_Comments::get_review_count_for_product( $product ); |
|
355 | 381 | } |
|
356 | |||
357 | if ( '' === $set_props['rating_counts'] ) { |
||
358 | unset( $set_props['rating_counts'] ); |
||
359 | WC_Comments::get_rating_counts_for_product( $product ); |
||
360 | 381 | } |
|
361 | |||
362 | if ( '' === $set_props['average_rating'] ) { |
||
363 | unset( $set_props['average_rating'] ); |
||
364 | WC_Comments::get_average_rating_for_product( $product ); |
||
365 | } |
||
366 | |||
367 | $product->set_props( $set_props ); |
||
368 | |||
369 | // Handle sale dates on the fly in case of missed cron schedule. |
||
370 | if ( $product->is_type( 'simple' ) && $product->is_on_sale( 'edit' ) && $product->get_sale_price( 'edit' ) !== $product->get_price( 'edit' ) ) { |
||
371 | update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); |
||
372 | 382 | $product->set_price( $product->get_sale_price( 'edit' ) ); |
|
373 | 382 | } |
|
374 | 21 | } |
|
375 | 21 | ||
376 | 21 | /** |
|
377 | * Read extra data associated with the product, like button text or product URL for external products. |
||
378 | * |
||
379 | * @param WC_Product $product Product object. |
||
380 | * @since 3.0.0 |
||
381 | */ |
||
382 | View Code Duplication | protected function read_extra_data( &$product ) { |
|
383 | foreach ( $product->get_extra_data_keys() as $key ) { |
||
384 | $function = 'set_' . $key; |
||
385 | if ( is_callable( array( $product, $function ) ) ) { |
||
386 | $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); |
||
387 | } |
||
388 | 381 | } |
|
389 | 381 | } |
|
390 | 381 | ||
391 | 381 | /** |
|
392 | 381 | * Convert visibility terms to props. |
|
393 | 381 | * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. |
|
394 | * |
||
395 | 381 | * @param WC_Product $product Product object. |
|
396 | 1 | * @since 3.0.0 |
|
397 | 381 | */ |
|
398 | protected function read_visibility( &$product ) { |
||
399 | 381 | $terms = get_the_terms( $product->get_id(), 'product_visibility' ); |
|
400 | 2 | $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); |
|
401 | $featured = in_array( 'featured', $term_names, true ); |
||
402 | 380 | $exclude_search = in_array( 'exclude-from-search', $term_names, true ); |
|
403 | $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); |
||
404 | |||
405 | 381 | View Code Duplication | if ( $exclude_search && $exclude_catalog ) { |
406 | $catalog_visibility = 'hidden'; |
||
407 | 381 | } elseif ( $exclude_search ) { |
|
408 | 381 | $catalog_visibility = 'catalog'; |
|
409 | } elseif ( $exclude_catalog ) { |
||
410 | $catalog_visibility = 'search'; |
||
411 | } else { |
||
412 | $catalog_visibility = 'visible'; |
||
413 | } |
||
414 | |||
415 | $product->set_props( |
||
416 | array( |
||
417 | 'featured' => $featured, |
||
418 | 337 | 'catalog_visibility' => $catalog_visibility, |
|
419 | 337 | ) |
|
420 | ); |
||
421 | 337 | } |
|
422 | 3 | ||
423 | 3 | /** |
|
424 | 3 | * Read attributes from post meta. |
|
425 | * |
||
426 | 3 | * @param WC_Product $product Product object. |
|
427 | */ |
||
428 | protected function read_attributes( &$product ) { |
||
429 | $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); |
||
430 | |||
431 | if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { |
||
432 | 3 | $attributes = array(); |
|
433 | foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { |
||
434 | $meta_value = array_merge( |
||
435 | array( |
||
436 | 3 | 'name' => '', |
|
437 | 1 | 'value' => '', |
|
438 | 'position' => 0, |
||
439 | 'is_visible' => 0, |
||
440 | 1 | 'is_variation' => 0, |
|
441 | 1 | 'is_taxonomy' => 0, |
|
442 | ), |
||
443 | 3 | (array) $meta_attribute_value |
|
444 | 3 | ); |
|
445 | |||
446 | // Check if is a taxonomy attribute. |
||
447 | 3 | View Code Duplication | if ( ! empty( $meta_value['is_taxonomy'] ) ) { |
448 | 3 | if ( ! taxonomy_exists( $meta_value['name'] ) ) { |
|
449 | 3 | continue; |
|
450 | 3 | } |
|
451 | 3 | $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); |
|
452 | 3 | $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); |
|
453 | 3 | } else { |
|
454 | 3 | $id = 0; |
|
455 | $options = wc_get_text_attributes( $meta_value['value'] ); |
||
456 | 3 | } |
|
457 | |||
458 | $attribute = new WC_Product_Attribute(); |
||
459 | $attribute->set_id( $id ); |
||
460 | $attribute->set_name( $meta_value['name'] ); |
||
461 | $attribute->set_options( $options ); |
||
462 | $attribute->set_position( $meta_value['position'] ); |
||
463 | $attribute->set_visible( $meta_value['is_visible'] ); |
||
464 | $attribute->set_variation( $meta_value['is_variation'] ); |
||
465 | $attributes[] = $attribute; |
||
466 | 382 | } |
|
467 | 382 | $product->set_attributes( $attributes ); |
|
468 | } |
||
469 | 382 | } |
|
470 | 3 | ||
471 | 3 | /** |
|
472 | 3 | * Read downloads from post meta. |
|
473 | * |
||
474 | * @param WC_Product $product Product object. |
||
475 | 3 | * @since 3.0.0 |
|
476 | 3 | */ |
|
477 | 3 | protected function read_downloads( &$product ) { |
|
478 | 3 | $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); |
|
479 | 3 | ||
480 | if ( $meta_values ) { |
||
481 | 3 | $downloads = array(); |
|
482 | foreach ( $meta_values as $key => $value ) { |
||
483 | if ( ! isset( $value['name'], $value['file'] ) ) { |
||
484 | continue; |
||
485 | } |
||
486 | $download = new WC_Product_Download(); |
||
487 | $download->set_id( $key ); |
||
488 | $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); |
||
489 | $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); |
||
490 | $downloads[] = $download; |
||
491 | } |
||
492 | 382 | $product->set_downloads( $downloads ); |
|
493 | } |
||
494 | 382 | } |
|
495 | |||
496 | /** |
||
497 | * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. |
||
498 | * |
||
499 | * @param WC_Product $product Product object. |
||
500 | * @param bool $force Force update. Used during create. |
||
501 | * @since 3.0.0 |
||
502 | */ |
||
503 | protected function update_post_meta( &$product, $force = false ) { |
||
504 | $meta_key_to_props = array( |
||
505 | '_sku' => 'sku', |
||
506 | '_regular_price' => 'regular_price', |
||
507 | '_sale_price' => 'sale_price', |
||
508 | '_sale_price_dates_from' => 'date_on_sale_from', |
||
509 | '_sale_price_dates_to' => 'date_on_sale_to', |
||
510 | 'total_sales' => 'total_sales', |
||
511 | '_tax_status' => 'tax_status', |
||
512 | '_tax_class' => 'tax_class', |
||
513 | '_manage_stock' => 'manage_stock', |
||
514 | '_backorders' => 'backorders', |
||
515 | '_low_stock_amount' => 'low_stock_amount', |
||
516 | '_sold_individually' => 'sold_individually', |
||
517 | '_weight' => 'weight', |
||
518 | '_length' => 'length', |
||
519 | '_width' => 'width', |
||
520 | '_height' => 'height', |
||
521 | '_upsell_ids' => 'upsell_ids', |
||
522 | '_crosssell_ids' => 'cross_sell_ids', |
||
523 | '_purchase_note' => 'purchase_note', |
||
524 | '_default_attributes' => 'default_attributes', |
||
525 | '_virtual' => 'virtual', |
||
526 | '_downloadable' => 'downloadable', |
||
527 | '_product_image_gallery' => 'gallery_image_ids', |
||
528 | 382 | '_download_limit' => 'download_limit', |
|
529 | '_download_expiry' => 'download_expiry', |
||
530 | 382 | '_thumbnail_id' => 'image_id', |
|
531 | 21 | '_stock' => 'stock_quantity', |
|
532 | '_stock_status' => 'stock_status', |
||
533 | '_wc_average_rating' => 'average_rating', |
||
534 | 382 | '_wc_rating_count' => 'rating_counts', |
|
535 | '_wc_review_count' => 'review_count', |
||
536 | 382 | ); |
|
537 | 382 | ||
538 | 382 | // Make sure to take extra data (like product url or text for external products) into account. |
|
539 | $extra_data_keys = $product->get_extra_data_keys(); |
||
540 | 382 | ||
541 | 382 | foreach ( $extra_data_keys as $key ) { |
|
542 | 382 | $meta_key_to_props[ '_' . $key ] = $key; |
|
543 | 382 | } |
|
544 | 382 | ||
545 | 382 | $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); |
|
546 | 382 | ||
547 | 382 | foreach ( $props_to_update as $meta_key => $prop ) { |
|
548 | 382 | $value = $product->{"get_$prop"}( 'edit' ); |
|
549 | 382 | $value = is_string( $value ) ? wp_slash( $value ) : $value; |
|
550 | 382 | switch ( $prop ) { |
|
551 | 10 | case 'virtual': |
|
552 | case 'downloadable': |
||
553 | 381 | case 'manage_stock': |
|
554 | case 'sold_individually': |
||
555 | 382 | $updated = update_post_meta( $product->get_id(), $meta_key, wc_bool_to_string( $value ) ); |
|
556 | 382 | break; |
|
557 | 382 | case 'gallery_image_ids': |
|
558 | 382 | $updated = update_post_meta( $product->get_id(), $meta_key, implode( ',', $value ) ); |
|
559 | 382 | break; |
|
560 | 382 | case 'image_id': |
|
561 | if ( ! empty( $value ) ) { |
||
562 | 382 | set_post_thumbnail( $product->get_id(), $value ); |
|
563 | 382 | } else { |
|
564 | delete_post_meta( $product->get_id(), '_thumbnail_id' ); |
||
565 | 382 | } |
|
566 | 382 | $updated = true; |
|
567 | break; |
||
568 | case 'date_on_sale_from': |
||
569 | case 'date_on_sale_to': |
||
570 | $updated = update_post_meta( $product->get_id(), $meta_key, $value ? $value->getTimestamp() : '' ); |
||
571 | 382 | break; |
|
572 | 382 | default: |
|
573 | 21 | $updated = update_post_meta( $product->get_id(), $meta_key, $value ); |
|
574 | 1 | break; |
|
575 | } |
||
576 | 21 | if ( $updated ) { |
|
577 | 21 | $this->updated_props[] = $prop; |
|
578 | 21 | } |
|
579 | 21 | } |
|
580 | |||
581 | 21 | // Update extra data associated with the product like button text or product URL for external products. |
|
582 | 21 | if ( ! $this->extra_data_saved ) { |
|
583 | foreach ( $extra_data_keys as $key ) { |
||
584 | if ( ! array_key_exists( '_' . $key, $props_to_update ) ) { |
||
585 | continue; |
||
586 | } |
||
587 | $function = 'get_' . $key; |
||
588 | 382 | if ( is_callable( array( $product, $function ) ) ) { |
|
589 | 382 | $value = $product->{$function}( 'edit' ); |
|
590 | $value = is_string( $value ) ? wp_slash( $value ) : $value; |
||
591 | |||
592 | if ( update_post_meta( $product->get_id(), '_' . $key, $value ) ) { |
||
593 | $this->updated_props[] = $key; |
||
594 | } |
||
595 | } |
||
596 | } |
||
597 | } |
||
598 | |||
599 | 382 | if ( $this->update_downloads( $product, $force ) ) { |
|
600 | 382 | $this->updated_props[] = 'downloads'; |
|
601 | } |
||
602 | 382 | } |
|
603 | 379 | ||
604 | 379 | /** |
|
605 | 40 | * Handle updated meta props after updating meta data. |
|
606 | 40 | * |
|
607 | * @since 3.0.0 |
||
608 | * @param WC_Product $product Product Object. |
||
609 | */ |
||
610 | 379 | protected function handle_updated_props( &$product ) { |
|
611 | 379 | $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) ); |
|
612 | 16 | ||
613 | 16 | if ( ! $price_is_synced ) { |
|
614 | if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) { |
||
615 | 379 | if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) { |
|
616 | 379 | update_post_meta( $product->get_id(), '_sale_price', '' ); |
|
617 | $product->set_sale_price( '' ); |
||
618 | } |
||
619 | } |
||
620 | |||
621 | 382 | if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) { |
|
622 | 382 | if ( $product->is_on_sale( 'edit' ) ) { |
|
623 | 60 | update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); |
|
624 | $product->set_price( $product->get_sale_price( 'edit' ) ); |
||
625 | 381 | } else { |
|
626 | update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); |
||
627 | $product->set_price( $product->get_regular_price( 'edit' ) ); |
||
628 | } |
||
629 | 382 | } |
|
630 | 382 | } |
|
631 | 60 | ||
632 | if ( in_array( 'stock_quantity', $this->updated_props, true ) ) { |
||
633 | 381 | if ( $product->is_type( 'variation' ) ) { |
|
634 | do_action( 'woocommerce_variation_set_stock', $product ); |
||
635 | } else { |
||
636 | do_action( 'woocommerce_product_set_stock', $product ); |
||
637 | } |
||
638 | 382 | } |
|
639 | |||
640 | if ( in_array( 'stock_status', $this->updated_props, true ) ) { |
||
641 | 382 | if ( $product->is_type( 'variation' ) ) { |
|
642 | do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); |
||
643 | } else { |
||
644 | do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); |
||
645 | } |
||
646 | } |
||
647 | |||
648 | // Trigger action so 3rd parties can deal with updated props. |
||
649 | do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); |
||
650 | |||
651 | 381 | // After handling, we can reset the props array. |
|
652 | 381 | $this->updated_props = array(); |
|
653 | } |
||
654 | 381 | ||
655 | 381 | /** |
|
656 | * For all stored terms in all taxonomies, save them to the DB. |
||
657 | 381 | * |
|
658 | 380 | * @param WC_Product $product Product object. |
|
659 | * @param bool $force Force update. Used during create. |
||
660 | * @since 3.0.0 |
||
661 | 381 | */ |
|
662 | protected function update_terms( &$product, $force = false ) { |
||
663 | 381 | $changes = $product->get_changes(); |
|
664 | 381 | ||
665 | if ( $force || array_key_exists( 'category_ids', $changes ) ) { |
||
666 | 381 | $categories = $product->get_category_ids( 'edit' ); |
|
667 | 381 | ||
668 | if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) { |
||
669 | $categories = array( get_option( 'default_product_cat', 0 ) ); |
||
670 | } |
||
671 | |||
672 | wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false ); |
||
673 | } |
||
674 | if ( $force || array_key_exists( 'tag_ids', $changes ) ) { |
||
675 | wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); |
||
676 | } |
||
677 | View Code Duplication | if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { |
|
678 | wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); |
||
679 | 381 | } |
|
680 | 381 | } |
|
681 | |||
682 | 381 | /** |
|
683 | 381 | * Update visibility terms based on props. |
|
684 | * |
||
685 | 381 | * @since 3.0.0 |
|
686 | 4 | * |
|
687 | * @param WC_Product $product Product object. |
||
688 | * @param bool $force Force update. Used during create. |
||
689 | 381 | */ |
|
690 | 66 | protected function update_visibility( &$product, $force = false ) { |
|
691 | $changes = $product->get_changes(); |
||
692 | |||
693 | 381 | if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { |
|
694 | $terms = array(); |
||
695 | 381 | ||
696 | 1 | if ( $product->get_featured() ) { |
|
697 | $terms[] = 'featured'; |
||
698 | } |
||
699 | 381 | ||
700 | 381 | if ( 'outofstock' === $product->get_stock_status() ) { |
|
701 | 1 | $terms[] = 'outofstock'; |
|
702 | 1 | } |
|
703 | 1 | ||
704 | 381 | $rating = min( 5, round( $product->get_average_rating(), 0 ) ); |
|
705 | |||
706 | if ( $rating > 0 ) { |
||
707 | 381 | $terms[] = 'rated-' . $rating; |
|
708 | 2 | } |
|
709 | 2 | ||
710 | switch ( $product->get_catalog_visibility() ) { |
||
711 | case 'hidden': |
||
712 | 381 | $terms[] = 'exclude-from-search'; |
|
713 | 381 | $terms[] = 'exclude-from-catalog'; |
|
714 | 381 | break; |
|
715 | case 'catalog': |
||
716 | $terms[] = 'exclude-from-search'; |
||
717 | break; |
||
718 | case 'search': |
||
719 | $terms[] = 'exclude-from-catalog'; |
||
720 | break; |
||
721 | } |
||
722 | |||
723 | if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { |
||
724 | do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); |
||
725 | } |
||
726 | 381 | } |
|
727 | 381 | } |
|
728 | |||
729 | 381 | /** |
|
730 | 381 | * Update attributes which are a mix of terms and meta data. |
|
731 | 381 | * |
|
732 | * @param WC_Product $product Product object. |
||
733 | 381 | * @param bool $force Force update. Used during create. |
|
734 | 53 | * @since 3.0.0 |
|
735 | 53 | */ |
|
736 | protected function update_attributes( &$product, $force = false ) { |
||
737 | 53 | $changes = $product->get_changes(); |
|
738 | |||
739 | 53 | if ( $force || array_key_exists( 'attributes', $changes ) ) { |
|
740 | $attributes = $product->get_attributes(); |
||
741 | $meta_values = array(); |
||
742 | |||
743 | if ( $attributes ) { |
||
744 | foreach ( $attributes as $attribute_key => $attribute ) { |
||
745 | $value = ''; |
||
746 | 53 | ||
747 | 46 | if ( is_null( $attribute ) ) { |
|
748 | if ( taxonomy_exists( $attribute_key ) ) { |
||
749 | 10 | // Handle attributes that have been unset. |
|
750 | wp_set_object_terms( $product->get_id(), array(), $attribute_key ); |
||
751 | } elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) { |
||
752 | // Handle attributes that have been unset. |
||
753 | 53 | wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) ); |
|
754 | 53 | } |
|
755 | 53 | continue; |
|
756 | 53 | ||
757 | 53 | } elseif ( $attribute->is_taxonomy() ) { |
|
758 | 53 | wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); |
|
759 | 53 | } else { |
|
760 | $value = wc_implode_text_attributes( $attribute->get_options() ); |
||
761 | } |
||
762 | |||
763 | 381 | // Store in format WC uses in meta. |
|
764 | $meta_values[ $attribute_key ] = array( |
||
765 | 'name' => $attribute->get_name(), |
||
766 | 'value' => $value, |
||
767 | 'position' => $attribute->get_position(), |
||
768 | 'is_visible' => $attribute->get_visible() ? 1 : 0, |
||
769 | 'is_variation' => $attribute->get_variation() ? 1 : 0, |
||
770 | 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, |
||
771 | ); |
||
772 | } |
||
773 | } |
||
774 | // Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround. |
||
775 | 382 | update_post_meta( $product->get_id(), '_product_attributes', wp_slash( $meta_values ) ); |
|
776 | 382 | } |
|
777 | } |
||
778 | 382 | ||
779 | 382 | /** |
|
780 | 382 | * Update downloads. |
|
781 | * |
||
782 | 382 | * @since 3.0.0 |
|
783 | 3 | * @param WC_Product $product Product object. |
|
784 | * @param bool $force Force update. Used during create. |
||
785 | 3 | * @return bool If updated or not. |
|
786 | */ |
||
787 | protected function update_downloads( &$product, $force = false ) { |
||
788 | $changes = $product->get_changes(); |
||
789 | 382 | ||
790 | 60 | if ( $force || array_key_exists( 'downloads', $changes ) ) { |
|
791 | $downloads = $product->get_downloads(); |
||
792 | 381 | $meta_values = array(); |
|
793 | |||
794 | if ( $downloads ) { |
||
795 | 382 | foreach ( $downloads as $key => $download ) { |
|
796 | // Store in format WC uses in meta. |
||
797 | 95 | $meta_values[ $key ] = $download->get_data(); |
|
798 | } |
||
799 | } |
||
800 | |||
801 | if ( $product->is_type( 'variation' ) ) { |
||
802 | do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); |
||
803 | } else { |
||
804 | do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); |
||
805 | } |
||
806 | 381 | ||
807 | 381 | return update_post_meta( $product->get_id(), '_downloadable_files', $meta_values ); |
|
808 | 381 | } |
|
809 | return false; |
||
810 | 381 | } |
|
811 | 381 | ||
812 | /** |
||
813 | * Make sure we store the product type and version (to track data changes). |
||
814 | 381 | * |
|
815 | 72 | * @param WC_Product $product Product object. |
|
816 | 72 | * @since 3.0.0 |
|
817 | */ |
||
818 | protected function update_version_and_type( &$product ) { |
||
819 | $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); |
||
820 | $new_type = $product->get_type(); |
||
821 | |||
822 | wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); |
||
823 | update_post_meta( $product->get_id(), '_product_version', WC_VERSION ); |
||
824 | |||
825 | // Action for the transition. |
||
826 | 382 | if ( $old_type !== $new_type ) { |
|
827 | 382 | $this->updated_props[] = 'product_type'; |
|
828 | 382 | do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); |
|
829 | } |
||
830 | } |
||
831 | |||
832 | /** |
||
833 | * Clear any caches. |
||
834 | * |
||
835 | * @param WC_Product $product Product object. |
||
836 | * @since 3.0.0 |
||
837 | */ |
||
838 | protected function clear_caches( &$product ) { |
||
839 | wc_delete_product_transients( $product->get_id() ); |
||
840 | if ( $product->get_parent_id( 'edit' ) ) { |
||
841 | wc_delete_product_transients( $product->get_parent_id( 'edit' ) ); |
||
842 | WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_parent_id( 'edit' ) ); |
||
843 | } |
||
844 | 4 | WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) ); |
|
845 | WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_id() ); |
||
846 | } |
||
847 | 4 | ||
848 | 4 | /* |
|
849 | 4 | |-------------------------------------------------------------------------- |
|
850 | 4 | | wc-product-functions.php methods |
|
851 | 4 | |-------------------------------------------------------------------------- |
|
852 | 4 | */ |
|
853 | |||
854 | 4 | /** |
|
855 | * Returns an array of on sale products, as an array of objects with an |
||
856 | * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. |
||
857 | * |
||
858 | 4 | * @return array |
|
859 | * @since 3.0.0 |
||
860 | */ |
||
861 | public function get_on_sale_products() { |
||
862 | global $wpdb; |
||
863 | |||
864 | 4 | $decimals = absint( wc_get_price_decimals() ); |
|
865 | 4 | $exclude_term_ids = array(); |
|
866 | $outofstock_join = ''; |
||
867 | $outofstock_where = ''; |
||
868 | $non_published_where = ''; |
||
869 | $product_visibility_term_ids = wc_get_product_visibility_term_ids(); |
||
870 | 4 | ||
871 | 1 | View Code Duplication | if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { |
872 | $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; |
||
873 | } |
||
874 | 4 | ||
875 | if ( count( $exclude_term_ids ) ) { |
||
876 | 4 | $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id'; |
|
877 | 4 | $outofstock_where = ' AND exclude_join.object_id IS NULL'; |
|
878 | 4 | } |
|
879 | 4 | ||
880 | 4 | // Fetch a list of non-published parent products and exlude them, quicker than joining in the main query below. |
|
881 | $non_published_products = $wpdb->get_col( |
||
882 | "SELECT post.ID as id FROM `$wpdb->posts` AS post |
||
883 | WHERE post.post_type = 'product' |
||
884 | AND post.post_parent = 0 |
||
885 | AND post.post_status != 'publish'" |
||
886 | ); |
||
887 | if ( 0 < count( $non_published_products ) ) { |
||
888 | 4 | $non_published_where = ' AND post.post_parent NOT IN ( ' . implode( ',', $non_published_products ) . ')'; |
|
889 | 4 | } |
|
890 | |||
891 | 4 | return $wpdb->get_results( |
|
892 | 4 | // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared |
|
893 | $wpdb->prepare( |
||
894 | "SELECT post.ID as id, post.post_parent as parent_id FROM `$wpdb->posts` AS post |
||
895 | LEFT JOIN `$wpdb->postmeta` AS meta ON post.ID = meta.post_id |
||
896 | LEFT JOIN `$wpdb->postmeta` AS meta2 ON post.ID = meta2.post_id |
||
897 | $outofstock_join |
||
898 | WHERE post.post_type IN ( 'product', 'product_variation' ) |
||
899 | AND post.post_status = 'publish' |
||
900 | AND meta.meta_key = '_sale_price' |
||
901 | AND meta2.meta_key = '_price' |
||
902 | AND CAST( meta.meta_value AS DECIMAL ) >= 0 |
||
903 | AND CAST( meta.meta_value AS CHAR ) != '' |
||
904 | AND CAST( meta.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) |
||
905 | $outofstock_where |
||
906 | 2 | $non_published_where |
|
907 | 2 | GROUP BY post.ID", |
|
908 | $decimals, |
||
909 | 2 | $decimals |
|
910 | ) |
||
911 | 2 | // phpcs:enable |
|
912 | ); |
||
913 | 2 | } |
|
914 | |||
915 | /** |
||
916 | 2 | * Returns a list of product IDs ( id as key => parent as value) that are |
|
917 | * featured. Uses get_posts instead of wc_get_products since we want |
||
918 | 2 | * some extra meta queries and ALL products (posts_per_page = -1). |
|
919 | 2 | * |
|
920 | 2 | * @return array |
|
921 | * @since 3.0.0 |
||
922 | */ |
||
923 | 2 | public function get_featured_product_ids() { |
|
924 | 2 | $product_visibility_term_ids = wc_get_product_visibility_term_ids(); |
|
925 | 2 | ||
926 | 2 | return get_posts( |
|
927 | array( |
||
928 | 'post_type' => array( 'product', 'product_variation' ), |
||
929 | 2 | 'posts_per_page' => -1, |
|
930 | 'post_status' => 'publish', |
||
931 | 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query |
||
932 | 'relation' => 'AND', |
||
933 | array( |
||
934 | 'taxonomy' => 'product_visibility', |
||
935 | 'field' => 'term_taxonomy_id', |
||
936 | 'terms' => array( $product_visibility_term_ids['featured'] ), |
||
937 | ), |
||
938 | array( |
||
939 | 'taxonomy' => 'product_visibility', |
||
940 | 'field' => 'term_taxonomy_id', |
||
941 | 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), |
||
942 | 328 | 'operator' => 'NOT IN', |
|
943 | ), |
||
944 | ), |
||
945 | 'fields' => 'id=>parent', |
||
946 | 328 | ) |
|
947 | 328 | ); |
|
948 | 328 | } |
|
949 | 328 | ||
950 | 328 | /** |
|
951 | 328 | * Check if product sku is found for any other product IDs. |
|
952 | 328 | * |
|
953 | 328 | * @since 3.0.0 |
|
954 | 328 | * @param int $product_id Product ID. |
|
955 | * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421. |
||
956 | 328 | * @return bool |
|
957 | */ |
||
958 | public function is_existing_sku( $product_id, $sku ) { |
||
959 | global $wpdb; |
||
960 | |||
961 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
962 | return $wpdb->get_var( |
||
963 | $wpdb->prepare( |
||
964 | "SELECT $wpdb->posts.ID |
||
965 | FROM $wpdb->posts |
||
966 | LEFT JOIN $wpdb->postmeta ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id ) |
||
967 | WHERE $wpdb->posts.post_type IN ( 'product', 'product_variation' ) |
||
968 | 82 | AND $wpdb->posts.post_status != 'trash' |
|
969 | AND $wpdb->postmeta.meta_key = '_sku' AND $wpdb->postmeta.meta_value = %s |
||
970 | AND $wpdb->postmeta.post_id <> %d |
||
971 | LIMIT 1", |
||
972 | 82 | wp_slash( $sku ), |
|
973 | 82 | $product_id |
|
974 | ) |
||
975 | 82 | ); |
|
976 | 82 | } |
|
977 | |||
978 | /** |
||
979 | * Return product ID based on SKU. |
||
980 | * |
||
981 | * @since 3.0.0 |
||
982 | 82 | * @param string $sku Product SKU. |
|
983 | * @return int |
||
984 | */ |
||
985 | View Code Duplication | public function get_product_id_by_sku( $sku ) { |
|
986 | 82 | global $wpdb; |
|
987 | |||
988 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
989 | $id = $wpdb->get_var( |
||
990 | $wpdb->prepare( |
||
991 | "SELECT posts.ID |
||
992 | FROM $wpdb->posts AS posts |
||
993 | LEFT JOIN $wpdb->postmeta AS postmeta ON ( posts.ID = postmeta.post_id ) |
||
994 | WHERE posts.post_type IN ( 'product', 'product_variation' ) |
||
995 | AND posts.post_status != 'trash' |
||
996 | AND postmeta.meta_key = '_sku' |
||
997 | AND postmeta.meta_value = %s |
||
998 | LIMIT 1", |
||
999 | $sku |
||
1000 | ) |
||
1001 | ); |
||
1002 | |||
1003 | return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku ); |
||
1004 | } |
||
1005 | |||
1006 | /** |
||
1007 | * Returns an array of IDs of products that have sales starting soon. |
||
1008 | * |
||
1009 | * @since 3.0.0 |
||
1010 | * @return array |
||
1011 | */ |
||
1012 | View Code Duplication | public function get_starting_sales() { |
|
1013 | global $wpdb; |
||
1014 | |||
1015 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1016 | return $wpdb->get_col( |
||
1017 | $wpdb->prepare( |
||
1018 | "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta |
||
1019 | LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id |
||
1020 | LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id |
||
1021 | WHERE postmeta.meta_key = '_sale_price_dates_from' |
||
1022 | AND postmeta_2.meta_key = '_price' |
||
1023 | AND postmeta_3.meta_key = '_sale_price' |
||
1024 | AND postmeta.meta_value > 0 |
||
1025 | AND postmeta.meta_value < %s |
||
1026 | AND postmeta_2.meta_value != postmeta_3.meta_value", |
||
1027 | current_time( 'timestamp', true ) |
||
1028 | ) |
||
1029 | ); |
||
1030 | } |
||
1031 | |||
1032 | /** |
||
1033 | * Returns an array of IDs of products that have sales which are due to end. |
||
1034 | * |
||
1035 | * @since 3.0.0 |
||
1036 | * @return array |
||
1037 | */ |
||
1038 | View Code Duplication | public function get_ending_sales() { |
|
1039 | global $wpdb; |
||
1040 | |||
1041 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1042 | return $wpdb->get_col( |
||
1043 | $wpdb->prepare( |
||
1044 | "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta |
||
1045 | LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id |
||
1046 | LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id |
||
1047 | WHERE postmeta.meta_key = '_sale_price_dates_to' |
||
1048 | AND postmeta_2.meta_key = '_price' |
||
1049 | AND postmeta_3.meta_key = '_regular_price' |
||
1050 | AND postmeta.meta_value > 0 |
||
1051 | AND postmeta.meta_value < %s |
||
1052 | AND postmeta_2.meta_value != postmeta_3.meta_value", |
||
1053 | current_time( 'timestamp', true ) - DAY_IN_SECONDS |
||
1054 | ) |
||
1055 | ); |
||
1056 | } |
||
1057 | |||
1058 | /** |
||
1059 | * Find a matching (enabled) variation within a variable product. |
||
1060 | * |
||
1061 | * @since 3.0.0 |
||
1062 | * @param WC_Product $product Variable product. |
||
1063 | * @param array $match_attributes Array of attributes we want to try to match. |
||
1064 | * @return int Matching variation ID or 0. |
||
1065 | */ |
||
1066 | public function find_matching_product_variation( $product, $match_attributes = array() ) { |
||
1067 | global $wpdb; |
||
1068 | |||
1069 | $meta_attribute_names = array(); |
||
1070 | |||
1071 | // Get attributes to match in meta. |
||
1072 | foreach ( $product->get_attributes() as $attribute ) { |
||
1073 | if ( ! $attribute->get_variation() ) { |
||
1074 | continue; |
||
1075 | } |
||
1076 | |||
1077 | $attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() ); |
||
1078 | |||
1079 | if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) { |
||
1080 | return 0; |
||
1081 | } |
||
1082 | |||
1083 | $meta_attribute_names[] = $attribute_field_name; |
||
1084 | } |
||
1085 | |||
1086 | // Get the attributes of the variations. |
||
1087 | $query = $wpdb->prepare( |
||
1088 | " |
||
1089 | SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta} |
||
1090 | WHERE post_id IN ( |
||
1091 | SELECT ID FROM {$wpdb->posts} |
||
1092 | WHERE {$wpdb->posts}.post_parent = %d |
||
1093 | AND {$wpdb->posts}.post_status = 'publish' |
||
1094 | AND {$wpdb->posts}.post_type = 'product_variation' |
||
1095 | ORDER BY menu_order ASC, ID ASC |
||
1096 | ) |
||
1097 | ", |
||
1098 | $product->get_id() |
||
1099 | ); |
||
1100 | |||
1101 | $query .= ' AND meta_key IN ( "' . implode( '","', array_map( 'esc_sql', $meta_attribute_names ) ) . '" );'; |
||
1102 | |||
1103 | $attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
1104 | |||
1105 | if ( ! $attributes ) { |
||
1106 | return 0; |
||
1107 | } |
||
1108 | |||
1109 | $sorted_meta = array(); |
||
1110 | |||
1111 | foreach ( $attributes as $m ) { |
||
1112 | $sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key |
||
0 ignored issues
–
show
|
|||
1113 | } |
||
1114 | |||
1115 | /** |
||
1116 | * Check each variation to find the one that matches the $match_attributes. |
||
1117 | * |
||
1118 | * Note: Not all meta fields will be set which is why we check existance. |
||
1119 | */ |
||
1120 | foreach ( $sorted_meta as $variation_id => $variation ) { |
||
1121 | $match = true; |
||
1122 | |||
1123 | foreach ( $match_attributes as $attribute_key => $attribute_value ) { |
||
1124 | if ( array_key_exists( $attribute_key, $variation ) ) { |
||
1125 | if ( $variation[ $attribute_key ] !== $attribute_value && ! empty( $variation[ $attribute_key ] ) ) { |
||
1126 | $match = false; |
||
1127 | } |
||
1128 | } |
||
1129 | } |
||
1130 | |||
1131 | if ( true === $match ) { |
||
1132 | return $variation_id; |
||
1133 | } |
||
1134 | } |
||
1135 | |||
1136 | if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { |
||
1137 | /** |
||
1138 | * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. |
||
1139 | * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. |
||
1140 | */ |
||
1141 | 20 | return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); |
|
1142 | } |
||
1143 | } |
||
1144 | |||
1145 | 20 | /** |
|
1146 | 20 | * Make sure all variations have a sort order set so they can be reordered correctly. |
|
1147 | 20 | * |
|
1148 | 20 | * @param int $parent_id Product ID. |
|
1149 | */ |
||
1150 | public function sort_all_product_variations( $parent_id ) { |
||
1151 | 20 | global $wpdb; |
|
1152 | |||
1153 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1154 | 20 | $ids = $wpdb->get_col( |
|
1155 | $wpdb->prepare( |
||
1156 | "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", |
||
1157 | $parent_id |
||
1158 | ) |
||
1159 | ); |
||
1160 | $index = 1; |
||
1161 | |||
1162 | foreach ( $ids as $id ) { |
||
1163 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1164 | $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); |
||
1165 | } |
||
1166 | } |
||
1167 | |||
1168 | /** |
||
1169 | 20 | * Return a list of related products (using data like categories and IDs). |
|
1170 | * |
||
1171 | * @since 3.0.0 |
||
1172 | 20 | * @param array $cats_array List of categories IDs. |
|
1173 | 20 | * @param array $tags_array List of tags IDs. |
|
1174 | 20 | * @param array $exclude_ids Excluded IDs. |
|
1175 | * @param int $limit Limit of results. |
||
1176 | 20 | * @param int $product_id Product ID. |
|
1177 | 20 | * @return array |
|
1178 | */ |
||
1179 | public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { |
||
1180 | 20 | global $wpdb; |
|
1181 | |||
1182 | $args = array( |
||
1183 | 'categories' => $cats_array, |
||
1184 | 'tags' => $tags_array, |
||
1185 | 'exclude_ids' => $exclude_ids, |
||
1186 | 20 | 'limit' => $limit + 10, |
|
1187 | ); |
||
1188 | 20 | ||
1189 | 20 | $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args ); |
|
1190 | |||
1191 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared |
||
1192 | return $wpdb->get_col( implode( ' ', $related_product_query ) ); |
||
1193 | } |
||
1194 | |||
1195 | /** |
||
1196 | 20 | * Builds the related posts query. |
|
1197 | * |
||
1198 | * @since 3.0.0 |
||
1199 | * |
||
1200 | 20 | * @param array $cats_array List of categories IDs. |
|
1201 | 20 | * @param array $tags_array List of tags IDs. |
|
1202 | 20 | * @param array $exclude_ids Excluded IDs. |
|
1203 | * @param int $limit Limit of results. |
||
1204 | * |
||
1205 | 20 | * @return array |
|
1206 | 20 | */ |
|
1207 | public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { |
||
1208 | global $wpdb; |
||
1209 | 20 | ||
1210 | 20 | $include_term_ids = array_merge( $cats_array, $tags_array ); |
|
1211 | $exclude_term_ids = array(); |
||
1212 | $product_visibility_term_ids = wc_get_product_visibility_term_ids(); |
||
1213 | 20 | ||
1214 | if ( $product_visibility_term_ids['exclude-from-catalog'] ) { |
||
1215 | $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; |
||
1216 | } |
||
1217 | |||
1218 | View Code Duplication | if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { |
|
1219 | $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; |
||
1220 | } |
||
1221 | |||
1222 | $query = array( |
||
1223 | 'fields' => " |
||
1224 | SELECT DISTINCT ID FROM {$wpdb->posts} p |
||
1225 | ", |
||
1226 | 2 | 'join' => '', |
|
1227 | 'where' => " |
||
1228 | 2 | WHERE 1=1 |
|
1229 | AND p.post_status = 'publish' |
||
1230 | AND p.post_type = 'product' |
||
1231 | |||
1232 | 2 | ", |
|
1233 | 'limits' => ' |
||
1234 | 1 | LIMIT ' . absint( $limit ) . ' |
|
1235 | 1 | ', |
|
1236 | 1 | ); |
|
1237 | |||
1238 | View Code Duplication | if ( count( $exclude_term_ids ) ) { |
|
1239 | 1 | $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; |
|
1240 | 2 | $query['where'] .= ' AND exclude_join.object_id IS NULL'; |
|
1241 | } |
||
1242 | 1 | ||
1243 | 1 | View Code Duplication | if ( count( $include_term_ids ) ) { |
1244 | 1 | $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; |
|
1245 | } |
||
1246 | |||
1247 | 1 | if ( count( $exclude_ids ) ) { |
|
1248 | $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )'; |
||
1249 | } |
||
1250 | 2 | ||
1251 | 2 | return $query; |
|
1252 | 2 | } |
|
1253 | |||
1254 | /** |
||
1255 | 2 | * Update a product's stock amount directly. |
|
1256 | * |
||
1257 | * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). |
||
1258 | 2 | * |
|
1259 | * @since 3.0.0 this supports set, increase and decrease. |
||
1260 | * @param int $product_id_with_stock Product ID. |
||
1261 | * @param int|null $stock_quantity Stock quantity. |
||
1262 | * @param string $operation Set, increase and decrease. |
||
1263 | */ |
||
1264 | public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { |
||
1265 | global $wpdb; |
||
1266 | add_post_meta( $product_id_with_stock, '_stock', 0, true ); |
||
1267 | |||
1268 | // Update stock in DB directly. |
||
1269 | switch ( $operation ) { |
||
1270 | case 'increase': |
||
1271 | 13 | $sql = $wpdb->prepare( |
|
1272 | "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'", |
||
1273 | 13 | $stock_quantity, |
|
1274 | $product_id_with_stock |
||
1275 | ); |
||
1276 | break; |
||
1277 | 13 | case 'decrease': |
|
1278 | $sql = $wpdb->prepare( |
||
1279 | 13 | "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'", |
|
1280 | 13 | $stock_quantity, |
|
1281 | 13 | $product_id_with_stock |
|
1282 | ); |
||
1283 | break; |
||
1284 | 13 | default: |
|
1285 | $sql = $wpdb->prepare( |
||
1286 | "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", |
||
1287 | $stock_quantity, |
||
1288 | $product_id_with_stock |
||
1289 | ); |
||
1290 | break; |
||
1291 | } |
||
1292 | |||
1293 | $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, $operation ); |
||
1294 | |||
1295 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared |
||
1296 | $wpdb->query( $sql ); |
||
1297 | |||
1298 | wp_cache_delete( $product_id_with_stock, 'post_meta' ); |
||
1299 | } |
||
1300 | |||
1301 | /** |
||
1302 | * Update a product's sale count directly. |
||
1303 | 13 | * |
|
1304 | * Uses queries rather than update_post_meta so we can do this in one query for performance. |
||
1305 | * |
||
1306 | * @since 3.0.0 this supports set, increase and decrease. |
||
1307 | * @param int $product_id Product ID. |
||
1308 | * @param int|null $quantity Quantity. |
||
1309 | * @param string $operation set, increase and decrease. |
||
1310 | */ |
||
1311 | public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { |
||
1312 | 22 | global $wpdb; |
|
1313 | 22 | add_post_meta( $product_id, 'total_sales', 0, true ); |
|
1314 | 22 | ||
1315 | // Update stock in DB directly. |
||
1316 | switch ( $operation ) { |
||
1317 | case 'increase': |
||
1318 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1319 | $wpdb->query( |
||
1320 | $wpdb->prepare( |
||
1321 | "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", |
||
1322 | $quantity, |
||
1323 | 22 | $product_id |
|
1324 | 22 | ) |
|
1325 | ); |
||
1326 | break; |
||
1327 | case 'decrease': |
||
1328 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1329 | $wpdb->query( |
||
1330 | $wpdb->prepare( |
||
1331 | "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", |
||
1332 | $quantity, |
||
1333 | 22 | $product_id |
|
1334 | 22 | ) |
|
1335 | ); |
||
1336 | break; |
||
1337 | default: |
||
1338 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1339 | $wpdb->query( |
||
1340 | $wpdb->prepare( |
||
1341 | "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", |
||
1342 | $quantity, |
||
1343 | $product_id |
||
1344 | 2 | ) |
|
1345 | 2 | ); |
|
1346 | 2 | break; |
|
1347 | 2 | } |
|
1348 | |||
1349 | wp_cache_delete( $product_id, 'post_meta' ); |
||
1350 | } |
||
1351 | |||
1352 | /** |
||
1353 | * Update a products average rating meta. |
||
1354 | * |
||
1355 | * @since 3.0.0 |
||
1356 | * @param WC_Product $product Product object. |
||
1357 | */ |
||
1358 | public function update_average_rating( $product ) { |
||
1359 | update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); |
||
1360 | self::update_visibility( $product, true ); |
||
1361 | } |
||
1362 | |||
1363 | /** |
||
1364 | * Update a products review count meta. |
||
1365 | * |
||
1366 | * @since 3.0.0 |
||
1367 | * @param WC_Product $product Product object. |
||
1368 | */ |
||
1369 | public function update_review_count( $product ) { |
||
1370 | update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); |
||
1371 | } |
||
1372 | |||
1373 | /** |
||
1374 | * Update a products rating counts. |
||
1375 | 1 | * |
|
1376 | * @since 3.0.0 |
||
1377 | * @param WC_Product $product Product object. |
||
1378 | 1 | */ |
|
1379 | 1 | public function update_rating_counts( $product ) { |
|
1380 | 1 | update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); |
|
1381 | 1 | } |
|
1382 | 1 | ||
1383 | 1 | /** |
|
1384 | 1 | * Get shipping class ID by slug. |
|
1385 | * |
||
1386 | * @since 3.0.0 |
||
1387 | 1 | * @param string $slug Product shipping class slug. |
|
1388 | 1 | * @return int|false |
|
1389 | */ |
||
1390 | 1 | public function get_shipping_class_id_by_slug( $slug ) { |
|
1391 | $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); |
||
1392 | if ( $shipping_class_term ) { |
||
1393 | 1 | return $shipping_class_term->term_id; |
|
1394 | 1 | } else { |
|
1395 | return false; |
||
1396 | 1 | } |
|
1397 | } |
||
1398 | 1 | ||
1399 | 1 | /** |
|
1400 | 1 | * Returns an array of products. |
|
1401 | * |
||
1402 | * @param array $args Args to pass to WC_Product_Query(). |
||
1403 | 1 | * @return array|object |
|
1404 | 1 | * @see wc_get_products |
|
1405 | */ |
||
1406 | public function get_products( $args = array() ) { |
||
1407 | $query = new WC_Product_Query( $args ); |
||
1408 | return $query->get_products(); |
||
1409 | } |
||
1410 | 1 | ||
1411 | 1 | /** |
|
1412 | * Search product data for a term and return ids. |
||
1413 | 1 | * |
|
1414 | 1 | * @param string $term Search term. |
|
1415 | 1 | * @param string $type Type of product. |
|
1416 | 1 | * @param bool $include_variations Include variations in search or not. |
|
1417 | * @param bool $all_statuses Should we search all statuses or limit to published. |
||
1418 | * @param null|int $limit Limit returned results. @since 3.5.0. |
||
1419 | 1 | * @return array of ids |
|
1420 | 1 | */ |
|
1421 | public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null ) { |
||
1422 | global $wpdb; |
||
1423 | |||
1424 | 1 | $custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit ); |
|
1425 | 1 | ||
1426 | if ( is_array( $custom_results ) ) { |
||
1427 | return $custom_results; |
||
1428 | 1 | } |
|
1429 | |||
1430 | $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); |
||
1431 | $post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ); |
||
1432 | $type_join = ''; |
||
1433 | 1 | $type_where = ''; |
|
1434 | $status_where = ''; |
||
1435 | $limit_query = ''; |
||
1436 | $term = wc_strtolower( $term ); |
||
1437 | 1 | ||
1438 | // See if search term contains OR keywords. |
||
1439 | if ( strstr( $term, ' or ' ) ) { |
||
1440 | $term_groups = explode( ' or ', $term ); |
||
1441 | } else { |
||
1442 | 1 | $term_groups = array( $term ); |
|
1443 | } |
||
1444 | 1 | ||
1445 | 1 | $search_where = ''; |
|
1446 | 1 | $search_queries = array(); |
|
1447 | 1 | ||
1448 | 1 | foreach ( $term_groups as $term_group ) { |
|
1449 | 1 | // Parse search terms. |
|
1450 | 1 | if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) { |
|
1451 | $search_terms = $this->get_valid_search_terms( $matches[0] ); |
||
1452 | 1 | $count = count( $search_terms ); |
|
1453 | |||
1454 | // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. |
||
1455 | if ( 9 < $count || 0 === $count ) { |
||
1456 | $search_terms = array( $term_group ); |
||
1457 | 1 | } |
|
1458 | } else { |
||
1459 | 1 | $search_terms = array( $term_group ); |
|
1460 | } |
||
1461 | |||
1462 | $term_group_query = ''; |
||
1463 | $searchand = ''; |
||
1464 | |||
1465 | foreach ( $search_terms as $search_term ) { |
||
1466 | $like = '%' . $wpdb->esc_like( $search_term ) . '%'; |
||
1467 | $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( postmeta.meta_key = '_sku' AND postmeta.meta_value LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine. |
||
1468 | $searchand = ' AND '; |
||
1469 | } |
||
1470 | |||
1471 | if ( $term_group_query ) { |
||
1472 | 1 | $search_queries[] = $term_group_query; |
|
1473 | } |
||
1474 | } |
||
1475 | |||
1476 | if ( ! empty( $search_queries ) ) { |
||
1477 | $search_where = 'AND (' . implode( ') OR (', $search_queries ) . ')'; |
||
1478 | } |
||
1479 | |||
1480 | if ( $type && in_array( $type, array( 'virtual', 'downloadable' ), true ) ) { |
||
1481 | $type_join = " LEFT JOIN {$wpdb->postmeta} postmeta_type ON posts.ID = postmeta_type.post_id "; |
||
1482 | 384 | $type_where = " AND ( postmeta_type.meta_key = '_{$type}' AND postmeta_type.meta_value = 'yes' ) "; |
|
1483 | 384 | } |
|
1484 | 384 | ||
1485 | 60 | if ( ! $all_statuses ) { |
|
1486 | 383 | $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') "; |
|
1487 | 381 | } |
|
1488 | 381 | ||
1489 | if ( $limit ) { |
||
1490 | 6 | $limit_query = $wpdb->prepare( ' LIMIT %d ', $limit ); |
|
1491 | } |
||
1492 | |||
1493 | // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery |
||
1494 | $search_results = $wpdb->get_results( |
||
1495 | // phpcs:disable |
||
1496 | "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts |
||
1497 | LEFT JOIN {$wpdb->postmeta} postmeta ON posts.ID = postmeta.post_id |
||
1498 | $type_join |
||
1499 | WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "') |
||
1500 | $search_where |
||
1501 | $status_where |
||
1502 | 1 | $type_where |
|
1503 | ORDER BY posts.post_parent ASC, posts.post_title ASC |
||
1504 | $limit_query |
||
1505 | 1 | " |
|
1506 | 1 | // phpcs:enable |
|
1507 | 1 | ); |
|
1508 | |||
1509 | 1 | $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) ); |
|
1510 | |||
1511 | if ( is_numeric( $term ) ) { |
||
1512 | $post_id = absint( $term ); |
||
1513 | 1 | $post_type = get_post_type( $post_id ); |
|
1514 | |||
1515 | if ( 'product_variation' === $post_type && $include_variations ) { |
||
1516 | $product_ids[] = $post_id; |
||
1517 | } elseif ( 'product' === $post_type ) { |
||
1518 | $product_ids[] = $post_id; |
||
1519 | } |
||
1520 | |||
1521 | $product_ids[] = wp_get_post_parent_id( $post_id ); |
||
1522 | } |
||
1523 | 14 | ||
1524 | return wp_parse_id_list( $product_ids ); |
||
1525 | } |
||
1526 | |||
1527 | 14 | /** |
|
1528 | * Get the product type based on product ID. |
||
1529 | * |
||
1530 | * @since 3.0.0 |
||
1531 | * @param int $product_id Product ID. |
||
1532 | * @return bool|string |
||
1533 | */ |
||
1534 | 14 | public function get_product_type( $product_id ) { |
|
1535 | 14 | $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id; |
|
1536 | 14 | $product_type = wp_cache_get( $cache_key, 'products' ); |
|
1537 | 14 | ||
1538 | if ( $product_type ) { |
||
1539 | return $product_type; |
||
1540 | } |
||
1541 | |||
1542 | $post_type = get_post_type( $product_id ); |
||
1543 | 14 | ||
1544 | if ( 'product_variation' === $post_type ) { |
||
1545 | $product_type = 'variation'; |
||
1546 | } elseif ( 'product' === $post_type ) { |
||
1547 | $terms = get_the_terms( $product_id, 'product_type' ); |
||
1548 | 14 | $product_type = ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; |
|
1549 | 14 | } else { |
|
1550 | 14 | $product_type = false; |
|
1551 | } |
||
1552 | |||
1553 | wp_cache_set( $cache_key, $product_type, 'products' ); |
||
1554 | |||
1555 | return $product_type; |
||
1556 | 14 | } |
|
1557 | |||
1558 | /** |
||
1559 | * Add ability to get products by 'reviews_allowed' in WC_Product_Query. |
||
1560 | 14 | * |
|
1561 | 14 | * @since 3.2.0 |
|
1562 | 14 | * @param string $where Where clause. |
|
1563 | 14 | * @param WP_Query $wp_query WP_Query instance. |
|
1564 | * @return string |
||
1565 | */ |
||
1566 | public function reviews_allowed_query_where( $where, $wp_query ) { |
||
1567 | 14 | global $wpdb; |
|
1568 | |||
1569 | 14 | if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) { |
|
1570 | 14 | if ( $wp_query->query_vars['reviews_allowed'] ) { |
|
1571 | $where .= " AND $wpdb->posts.comment_status = 'open'"; |
||
1572 | 14 | } else { |
|
1573 | $where .= " AND $wpdb->posts.comment_status = 'closed'"; |
||
1574 | } |
||
1575 | } |
||
1576 | |||
1577 | return $where; |
||
1578 | 14 | } |
|
1579 | 1 | ||
1580 | 14 | /** |
|
1581 | 1 | * Get valid WP_Query args from a WC_Product_Query's query variables. |
|
1582 | 1 | * |
|
1583 | 1 | * @since 3.2.0 |
|
1584 | * @param array $query_vars Query vars from a WC_Product_Query. |
||
1585 | 1 | * @return array |
|
1586 | 1 | */ |
|
1587 | 1 | protected function get_wp_query_args( $query_vars ) { |
|
1588 | |||
1589 | // Map query vars to ones that get_wp_query_args or WP_Query recognize. |
||
1590 | $key_mapping = array( |
||
1591 | 'status' => 'post_status', |
||
1592 | 'page' => 'paged', |
||
1593 | 'include' => 'post__in', |
||
1594 | 'stock_quantity' => 'stock', |
||
1595 | 'average_rating' => 'wc_average_rating', |
||
1596 | 13 | 'review_count' => 'wc_review_count', |
|
1597 | 13 | ); |
|
1598 | 13 | View Code Duplication | foreach ( $key_mapping as $query_key => $db_key ) { |
1599 | 13 | if ( isset( $query_vars[ $query_key ] ) ) { |
|
1600 | 13 | $query_vars[ $db_key ] = $query_vars[ $query_key ]; |
|
1601 | unset( $query_vars[ $query_key ] ); |
||
1602 | } |
||
1603 | } |
||
1604 | |||
1605 | 14 | // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'. |
|
1606 | 1 | $boolean_queries = array( |
|
1607 | 1 | 'virtual', |
|
1608 | 1 | 'downloadable', |
|
1609 | 1 | 'sold_individually', |
|
1610 | 'manage_stock', |
||
1611 | ); |
||
1612 | foreach ( $boolean_queries as $boolean_query ) { |
||
1613 | if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) { |
||
1614 | 14 | $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no'; |
|
1615 | 1 | } |
|
1616 | 1 | } |
|
1617 | 1 | ||
1618 | 1 | // These queries cannot be auto-generated so we have to remove them and build them manually. |
|
1619 | 1 | $manual_queries = array( |
|
1620 | 'sku' => '', |
||
1621 | 'featured' => '', |
||
1622 | 'visibility' => '', |
||
1623 | ); |
||
1624 | 14 | foreach ( $manual_queries as $key => $manual_query ) { |
|
1625 | 1 | if ( isset( $query_vars[ $key ] ) ) { |
|
1626 | 1 | $manual_queries[ $key ] = $query_vars[ $key ]; |
|
1627 | 1 | unset( $query_vars[ $key ] ); |
|
1628 | 1 | } |
|
1629 | } |
||
1630 | |||
1631 | $wp_query_args = parent::get_wp_query_args( $query_vars ); |
||
1632 | |||
1633 | if ( ! isset( $wp_query_args['date_query'] ) ) { |
||
1634 | 14 | $wp_query_args['date_query'] = array(); |
|
1635 | 1 | } |
|
1636 | 1 | if ( ! isset( $wp_query_args['meta_query'] ) ) { |
|
1637 | 1 | $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
|
1638 | 1 | } |
|
1639 | |||
1640 | // Handle product types. |
||
1641 | if ( 'variation' === $query_vars['type'] ) { |
||
1642 | $wp_query_args['post_type'] = 'product_variation'; |
||
1643 | 14 | } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) { |
|
1644 | $wp_query_args['post_type'] = array( 'product_variation', 'product' ); |
||
1645 | 2 | $wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query |
|
1646 | 1 | 'relation' => 'OR', |
|
1647 | array( |
||
1648 | 1 | 'taxonomy' => 'product_type', |
|
1649 | 'field' => 'slug', |
||
1650 | 'terms' => $query_vars['type'], |
||
1651 | ), |
||
1652 | array( |
||
1653 | 'taxonomy' => 'product_type', |
||
1654 | 'field' => 'id', |
||
1655 | 'operator' => 'NOT EXISTS', |
||
1656 | ), |
||
1657 | ); |
||
1658 | 2 | View Code Duplication | } else { |
1659 | 2 | $wp_query_args['post_type'] = 'product'; |
|
1660 | 2 | $wp_query_args['tax_query'][] = array( |
|
1661 | 2 | 'taxonomy' => 'product_type', |
|
1662 | 'field' => 'slug', |
||
1663 | 'terms' => $query_vars['type'], |
||
1664 | ); |
||
1665 | } |
||
1666 | |||
1667 | 14 | // Handle product categories. |
|
1668 | 1 | View Code Duplication | if ( ! empty( $query_vars['category'] ) ) { |
1669 | 1 | $wp_query_args['tax_query'][] = array( |
|
1670 | 1 | 'taxonomy' => 'product_cat', |
|
1671 | 1 | 'field' => 'slug', |
|
1672 | 1 | 'terms' => $query_vars['category'], |
|
1673 | 1 | ); |
|
1674 | } |
||
1675 | 1 | ||
1676 | 1 | // Handle product tags. |
|
1677 | 1 | View Code Duplication | if ( ! empty( $query_vars['tag'] ) ) { |
1678 | 1 | unset( $wp_query_args['tag'] ); |
|
1679 | 1 | $wp_query_args['tax_query'][] = array( |
|
1680 | 'taxonomy' => 'product_tag', |
||
1681 | 'field' => 'slug', |
||
1682 | 1 | 'terms' => $query_vars['tag'], |
|
1683 | 1 | ); |
|
1684 | 1 | } |
|
1685 | 1 | ||
1686 | 1 | // Handle shipping classes. |
|
1687 | View Code Duplication | if ( ! empty( $query_vars['shipping_class'] ) ) { |
|
1688 | $wp_query_args['tax_query'][] = array( |
||
1689 | 'taxonomy' => 'product_shipping_class', |
||
1690 | 'field' => 'slug', |
||
1691 | 'terms' => $query_vars['shipping_class'], |
||
1692 | 14 | ); |
|
1693 | 1 | } |
|
1694 | 1 | ||
1695 | 1 | // Handle total_sales. |
|
1696 | // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix. |
||
1697 | if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) { |
||
1698 | $wp_query_args['meta_query'][] = array( |
||
1699 | 'key' => 'total_sales', |
||
1700 | 'value' => absint( $query_vars['total_sales'] ), |
||
1701 | 1 | 'compare' => '=', |
|
1702 | 1 | ); |
|
1703 | } |
||
1704 | |||
1705 | // Handle SKU. |
||
1706 | if ( $manual_queries['sku'] ) { |
||
1707 | // Check for existing values if wildcard is used. |
||
1708 | if ( '*' === $manual_queries['sku'] ) { |
||
1709 | $wp_query_args['meta_query'][] = array( |
||
1710 | 1 | array( |
|
1711 | 1 | 'key' => '_sku', |
|
1712 | 'compare' => 'EXISTS', |
||
1713 | ), |
||
1714 | array( |
||
1715 | 'key' => '_sku', |
||
1716 | 'value' => '', |
||
1717 | 1 | 'compare' => '!=', |
|
1718 | 1 | ), |
|
1719 | 1 | ); |
|
1720 | } else { |
||
1721 | $wp_query_args['meta_query'][] = array( |
||
1722 | 'key' => '_sku', |
||
1723 | 'value' => $manual_queries['sku'], |
||
1724 | 'compare' => 'LIKE', |
||
1725 | 1 | ); |
|
1726 | } |
||
1727 | } |
||
1728 | |||
1729 | // Handle featured. |
||
1730 | if ( '' !== $manual_queries['featured'] ) { |
||
1731 | 14 | $product_visibility_term_ids = wc_get_product_visibility_term_ids(); |
|
1732 | if ( $manual_queries['featured'] ) { |
||
1733 | $wp_query_args['tax_query'][] = array( |
||
1734 | 'taxonomy' => 'product_visibility', |
||
1735 | 'field' => 'term_taxonomy_id', |
||
1736 | 14 | 'terms' => array( $product_visibility_term_ids['featured'] ), |
|
1737 | 14 | ); |
|
1738 | $wp_query_args['tax_query'][] = array( |
||
1739 | 'taxonomy' => 'product_visibility', |
||
1740 | 1 | 'field' => 'term_taxonomy_id', |
|
1741 | 1 | 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), |
|
1742 | 'operator' => 'NOT IN', |
||
1743 | ); |
||
1744 | } else { |
||
1745 | 14 | $wp_query_args['tax_query'][] = array( |
|
1746 | 'taxonomy' => 'product_visibility', |
||
1747 | 'field' => 'term_taxonomy_id', |
||
1748 | 'terms' => array( $product_visibility_term_ids['featured'] ), |
||
1749 | 'operator' => 'NOT IN', |
||
1750 | 14 | ); |
|
1751 | 13 | } |
|
1752 | } |
||
1753 | |||
1754 | // Handle visibility. |
||
1755 | 14 | if ( $manual_queries['visibility'] ) { |
|
1756 | 1 | switch ( $manual_queries['visibility'] ) { |
|
1757 | View Code Duplication | case 'search': |
|
1758 | $wp_query_args['tax_query'][] = array( |
||
1759 | 14 | 'taxonomy' => 'product_visibility', |
|
1760 | 'field' => 'slug', |
||
1761 | 'terms' => array( 'exclude-from-search' ), |
||
1762 | 'operator' => 'NOT IN', |
||
1763 | ); |
||
1764 | break; |
||
1765 | View Code Duplication | case 'catalog': |
|
1766 | $wp_query_args['tax_query'][] = array( |
||
1767 | 'taxonomy' => 'product_visibility', |
||
1768 | 'field' => 'slug', |
||
1769 | 'terms' => array( 'exclude-from-catalog' ), |
||
1770 | 'operator' => 'NOT IN', |
||
1771 | 14 | ); |
|
1772 | 14 | break; |
|
1773 | View Code Duplication | case 'visible': |
|
1774 | 14 | $wp_query_args['tax_query'][] = array( |
|
1775 | 'taxonomy' => 'product_visibility', |
||
1776 | 'field' => 'slug', |
||
1777 | 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), |
||
1778 | 'operator' => 'NOT IN', |
||
1779 | ); |
||
1780 | break; |
||
1781 | 14 | View Code Duplication | case 'hidden': |
1782 | $wp_query_args['tax_query'][] = array( |
||
1783 | 'taxonomy' => 'product_visibility', |
||
1784 | 14 | 'field' => 'slug', |
|
1785 | 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), |
||
1786 | 4 | 'operator' => 'AND', |
|
1787 | ); |
||
1788 | break; |
||
1789 | 14 | } |
|
1790 | } |
||
1791 | 14 | ||
1792 | // Handle date queries. |
||
1793 | 2 | $date_queries = array( |
|
1794 | 2 | 'date_created' => 'post_date', |
|
1795 | 2 | 'date_modified' => 'post_modified', |
|
1796 | 'date_on_sale_from' => '_sale_price_dates_from', |
||
1797 | 'date_on_sale_to' => '_sale_price_dates_to', |
||
1798 | ); |
||
1799 | 13 | foreach ( $date_queries as $query_var_key => $db_key ) { |
|
1800 | if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { |
||
1801 | |||
1802 | // Remove any existing meta queries for the same keys to prevent conflicts. |
||
1803 | $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); |
||
1804 | foreach ( $existing_queries as $query_index => $query_contents ) { |
||
1805 | unset( $wp_query_args['meta_query'][ $query_index ] ); |
||
1806 | } |
||
1807 | |||
1808 | $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); |
||
1809 | } |
||
1810 | } |
||
1811 | |||
1812 | // Handle paginate. |
||
1813 | View Code Duplication | if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { |
|
1814 | $wp_query_args['no_found_rows'] = true; |
||
1815 | } |
||
1816 | |||
1817 | // Handle reviews_allowed. |
||
1818 | if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) { |
||
1819 | add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 ); |
||
1820 | } |
||
1821 | |||
1822 | return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this ); |
||
1823 | } |
||
1824 | |||
1825 | /** |
||
1826 | * Query for Products matching specific criteria. |
||
1827 | * |
||
1828 | * @since 3.2.0 |
||
1829 | * |
||
1830 | * @param array $query_vars Query vars from a WC_Product_Query. |
||
1831 | * |
||
1832 | * @return array|object |
||
1833 | */ |
||
1834 | public function query( $query_vars ) { |
||
1835 | $args = $this->get_wp_query_args( $query_vars ); |
||
1836 | |||
1837 | View Code Duplication | if ( ! empty( $args['errors'] ) ) { |
|
1838 | $query = (object) array( |
||
1839 | 'posts' => array(), |
||
1840 | 'found_posts' => 0, |
||
1841 | 'max_num_pages' => 0, |
||
1842 | ); |
||
1843 | } else { |
||
1844 | $query = new WP_Query( $args ); |
||
1845 | } |
||
1846 | |||
1847 | if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) { |
||
1848 | // Prime caches before grabbing objects. |
||
1849 | update_post_caches( $query->posts, array( 'product', 'product_variation' ) ); |
||
1850 | } |
||
1851 | |||
1852 | $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) ); |
||
1853 | |||
1854 | View Code Duplication | if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { |
|
1855 | return (object) array( |
||
1856 | 'products' => $products, |
||
1857 | 'total' => $query->found_posts, |
||
1858 | 'max_num_pages' => $query->max_num_pages, |
||
1859 | ); |
||
1860 | } |
||
1861 | |||
1862 | return $products; |
||
1863 | } |
||
1864 | } |
||
1865 |