Completed
Push — master ( 08c7be...bf151a )
by
unknown
25:52 queued 10s
created

WC_Product_Data_Store_CPT::create_all_product_variations()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 8.0368

Importance

Changes 0
Metric Value
cc 8
nc 10
nop 2
dl 0
loc 44
ccs 22
cts 24
cp 0.9167
crap 8.0368
rs 7.9715
c 0
b 0
f 0
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
	 * Meta data which should exist in the DB, even if empty.
72
	 *
73
	 * @since 3.6.0
74
	 *
75
	 * @var array
76
	 */
77
	protected $must_exist_meta_keys = array(
78
		'_tax_class',
79
	);
80
81
	/**
82
	 * If we have already saved our extra data, don't do automatic / default handling.
83
	 *
84
	 * @var bool
85
	 */
86
	protected $extra_data_saved = false;
87
88
	/**
89
	 * Stores updated props.
90
	 *
91
	 * @var array
92
	 */
93
	protected $updated_props = array();
94
95
	/*
96
	|--------------------------------------------------------------------------
97
	| CRUD Methods
98
	|--------------------------------------------------------------------------
99
	*/
100
101
	/**
102
	 * Method to create a new product in the database.
103
	 *
104
	 * @param WC_Product $product Product object.
105
	 */
106 398
	public function create( &$product ) {
107 398
		if ( ! $product->get_date_created( 'edit' ) ) {
108 398
			$product->set_date_created( current_time( 'timestamp', true ) );
109
		}
110
111 398
		$id = wp_insert_post(
112 398
			apply_filters(
113 398
				'woocommerce_new_product_data',
114
				array(
115 398
					'post_type'      => 'product',
116 398
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
117 398
					'post_author'    => get_current_user_id(),
118 398
					'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
119 398
					'post_content'   => $product->get_description(),
120 398
					'post_excerpt'   => $product->get_short_description(),
121 398
					'post_parent'    => $product->get_parent_id(),
122 398
					'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
123 398
					'ping_status'    => 'closed',
124 398
					'menu_order'     => $product->get_menu_order(),
125 398
					'post_password'  => $product->get_post_password( 'edit' ),
126 398
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
127 398
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
128 398
					'post_name'      => $product->get_slug( 'edit' ),
129
				)
130
			),
131 398
			true
132
		);
133
134 398 View Code Duplication
		if ( $id && ! is_wp_error( $id ) ) {
135 398
			$product->set_id( $id );
136
137 398
			$this->update_post_meta( $product, true );
138 398
			$this->update_terms( $product, true );
139 398
			$this->update_visibility( $product, true );
140 398
			$this->update_attributes( $product, true );
141 398
			$this->update_version_and_type( $product );
142 398
			$this->handle_updated_props( $product );
143 398
			$this->clear_caches( $product );
144
145 398
			$product->save_meta_data();
146 398
			$product->apply_changes();
147
148 398
			do_action( 'woocommerce_new_product', $id );
149
		}
150
	}
151
152
	/**
153
	 * Method to read a product from the database.
154
	 *
155
	 * @param WC_Product $product Product object.
156
	 * @throws Exception If invalid product.
157
	 */
158 370
	public function read( &$product ) {
159 370
		$product->set_defaults();
160 370
		$post_object = get_post( $product->get_id() );
161
162 370 View Code Duplication
		if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) {
163 4
			throw new Exception( __( 'Invalid product.', 'woocommerce' ) );
164
		}
165
166 368
		$product->set_props(
167
			array(
168 368
				'name'              => $post_object->post_title,
169 368
				'slug'              => $post_object->post_name,
170 368
				'date_created'      => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
171 368
				'date_modified'     => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
172 368
				'status'            => $post_object->post_status,
173 368
				'description'       => $post_object->post_content,
174 368
				'short_description' => $post_object->post_excerpt,
175 368
				'parent_id'         => $post_object->post_parent,
176 368
				'menu_order'        => $post_object->menu_order,
177 368
				'post_password'     => $post_object->post_password,
178 368
				'reviews_allowed'   => 'open' === $post_object->comment_status,
179
			)
180
		);
181
182 368
		$this->read_attributes( $product );
183 368
		$this->read_downloads( $product );
184 368
		$this->read_visibility( $product );
185 368
		$this->read_product_data( $product );
186 368
		$this->read_extra_data( $product );
187 368
		$product->set_object_read( true );
188
	}
189
190
	/**
191
	 * Method to update a product in the database.
192
	 *
193
	 * @param WC_Product $product Product object.
194
	 */
195 109
	public function update( &$product ) {
196 109
		$product->save_meta_data();
197 109
		$changes = $product->get_changes();
198
199
		// Only update the post when the post data changes.
200 109
		if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) {
201
			$post_data = array(
202 14
				'post_content'   => $product->get_description( 'edit' ),
203 14
				'post_excerpt'   => $product->get_short_description( 'edit' ),
204 14
				'post_title'     => $product->get_name( 'edit' ),
205 14
				'post_parent'    => $product->get_parent_id( 'edit' ),
206 14
				'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
207 14
				'post_status'    => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
208 14
				'menu_order'     => $product->get_menu_order( 'edit' ),
209 14
				'post_password'  => $product->get_post_password( 'edit' ),
210 14
				'post_name'      => $product->get_slug( 'edit' ),
211 14
				'post_type'      => 'product',
212
			);
213 14
			if ( $product->get_date_created( 'edit' ) ) {
214 14
				$post_data['post_date']     = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
215 14
				$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
216
			}
217 14
			if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
218 2
				$post_data['post_modified']     = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
219 2
				$post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() );
220
			} else {
221 12
				$post_data['post_modified']     = current_time( 'mysql' );
222 12
				$post_data['post_modified_gmt'] = current_time( 'mysql', 1 );
223
			}
224
225
			/**
226
			 * When updating this object, to prevent infinite loops, use $wpdb
227
			 * to update data, since wp_update_post spawns more calls to the
228
			 * save_post action.
229
			 *
230
			 * This ensures hooks are fired by either WP itself (admin screen save),
231
			 * or an update purely from CRUD.
232
			 */
233 14 View Code Duplication
			if ( doing_action( 'save_post' ) ) {
234
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
235
				clean_post_cache( $product->get_id() );
236
			} else {
237 14
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
238
			}
239 14
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
240
241 View Code Duplication
		} else { // Only update post modified time to record this save event.
242 101
			$GLOBALS['wpdb']->update(
243 101
				$GLOBALS['wpdb']->posts,
244
				array(
245 101
					'post_modified'     => current_time( 'mysql' ),
246 101
					'post_modified_gmt' => current_time( 'mysql', 1 ),
247
				),
248
				array(
249 101
					'ID' => $product->get_id(),
250
				)
251
			);
252 101
			clean_post_cache( $product->get_id() );
253
		}
254
255 109
		$this->update_post_meta( $product );
256 109
		$this->update_terms( $product );
257 109
		$this->update_visibility( $product );
258 109
		$this->update_attributes( $product );
259 109
		$this->update_version_and_type( $product );
260 109
		$this->handle_updated_props( $product );
261 109
		$this->clear_caches( $product );
262
263 109
		$product->apply_changes();
264
265 109
		do_action( 'woocommerce_update_product', $product->get_id() );
266
	}
267
268
	/**
269
	 * Method to delete a product from the database.
270
	 *
271
	 * @param WC_Product $product Product object.
272
	 * @param array      $args Array of args to pass to the delete method.
273
	 */
274 29
	public function delete( &$product, $args = array() ) {
275 29
		$id        = $product->get_id();
276 29
		$post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product';
277
278 29
		$args = wp_parse_args(
279 29
			$args,
280
			array(
281 29
				'force_delete' => false,
282
			)
283
		);
284
285 29
		if ( ! $id ) {
286
			return;
287
		}
288
289 29
		if ( $args['force_delete'] ) {
290 27
			do_action( 'woocommerce_before_delete_' . $post_type, $id );
291 27
			wp_delete_post( $id );
292 27
			$product->set_id( 0 );
293 27
			do_action( 'woocommerce_delete_' . $post_type, $id );
294
		} else {
295 2
			wp_trash_post( $id );
296 2
			$product->set_status( 'trash' );
297 2
			do_action( 'woocommerce_trash_' . $post_type, $id );
298
		}
299
	}
300
301
	/*
302
	|--------------------------------------------------------------------------
303
	| Additional Methods
304
	|--------------------------------------------------------------------------
305
	*/
306
307
	/**
308
	 * Read product data. Can be overridden by child classes to load other props.
309
	 *
310
	 * @param WC_Product $product Product object.
311
	 * @since 3.0.0
312
	 */
313 368
	protected function read_product_data( &$product ) {
314 368
		$id                = $product->get_id();
315 368
		$post_meta_values  = get_post_meta( $id );
316
		$meta_key_to_props = array(
317 368
			'_sku'                   => 'sku',
318
			'_regular_price'         => 'regular_price',
319
			'_sale_price'            => 'sale_price',
320
			'_price'                 => 'price',
321
			'_sale_price_dates_from' => 'date_on_sale_from',
322
			'_sale_price_dates_to'   => 'date_on_sale_to',
323
			'total_sales'            => 'total_sales',
324
			'_tax_status'            => 'tax_status',
325
			'_tax_class'             => 'tax_class',
326
			'_manage_stock'          => 'manage_stock',
327
			'_backorders'            => 'backorders',
328
			'_low_stock_amount'      => 'low_stock_amount',
329
			'_sold_individually'     => 'sold_individually',
330
			'_weight'                => 'weight',
331
			'_length'                => 'length',
332
			'_width'                 => 'width',
333
			'_height'                => 'height',
334
			'_upsell_ids'            => 'upsell_ids',
335
			'_crosssell_ids'         => 'cross_sell_ids',
336
			'_purchase_note'         => 'purchase_note',
337
			'_default_attributes'    => 'default_attributes',
338
			'_virtual'               => 'virtual',
339
			'_downloadable'          => 'downloadable',
340
			'_download_limit'        => 'download_limit',
341
			'_download_expiry'       => 'download_expiry',
342
			'_thumbnail_id'          => 'image_id',
343
			'_stock'                 => 'stock_quantity',
344
			'_stock_status'          => 'stock_status',
345
			'_wc_average_rating'     => 'average_rating',
346
			'_wc_rating_count'       => 'rating_counts',
347
			'_wc_review_count'       => 'review_count',
348
			'_product_image_gallery' => 'gallery_image_ids',
349
		);
350
351 368
		$set_props = array();
352
353 368
		foreach ( $meta_key_to_props as $meta_key => $prop ) {
354 368
			$meta_value         = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null;
355 368
			$set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
356
		}
357
358 368
		$set_props['category_ids']      = $this->get_term_ids( $product, 'product_cat' );
359 368
		$set_props['tag_ids']           = $this->get_term_ids( $product, 'product_tag' );
360 368
		$set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) );
361 368
		$set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) );
362
363 368
		$product->set_props( $set_props );
364
	}
365
366
	/**
367
	 * Read extra data associated with the product, like button text or product URL for external products.
368
	 *
369
	 * @param WC_Product $product Product object.
370
	 * @since 3.0.0
371
	 */
372 374 View Code Duplication
	protected function read_extra_data( &$product ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
373 374
		foreach ( $product->get_extra_data_keys() as $key ) {
374 22
			$function = 'set_' . $key;
375 22
			if ( is_callable( array( $product, $function ) ) ) {
376 22
				$product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
377
			}
378
		}
379
	}
380
381
	/**
382
	 * Convert visibility terms to props.
383
	 * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
384
	 *
385
	 * @param WC_Product $product Product object.
386
	 * @since 3.0.0
387
	 */
388 368
	protected function read_visibility( &$product ) {
389 368
		$terms           = get_the_terms( $product->get_id(), 'product_visibility' );
390 368
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
391 368
		$featured        = in_array( 'featured', $term_names, true );
392 368
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
393 368
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
394
395 368 View Code Duplication
		if ( $exclude_search && $exclude_catalog ) {
396 1
			$catalog_visibility = 'hidden';
397 368
		} elseif ( $exclude_search ) {
398 1
			$catalog_visibility = 'catalog';
399 368
		} elseif ( $exclude_catalog ) {
400 2
			$catalog_visibility = 'search';
401
		} else {
402 367
			$catalog_visibility = 'visible';
403
		}
404
405 368
		$product->set_props(
406
			array(
407 368
				'featured'           => $featured,
408 368
				'catalog_visibility' => $catalog_visibility,
409
			)
410
		);
411
	}
412
413
	/**
414
	 * Read attributes from post meta.
415
	 *
416
	 * @param WC_Product $product Product object.
417
	 */
418 327
	protected function read_attributes( &$product ) {
419 327
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
420
421 327
		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
422 3
			$attributes = array();
423 3
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
424 3
				$meta_value = array_merge(
425
					array(
426 3
						'name'         => '',
427
						'value'        => '',
428
						'position'     => 0,
429
						'is_visible'   => 0,
430
						'is_variation' => 0,
431
						'is_taxonomy'  => 0,
432
					),
433 3
					(array) $meta_attribute_value
434
				);
435
436
				// Check if is a taxonomy attribute.
437 3 View Code Duplication
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
438
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
439
						continue;
440
					}
441
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
442
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
443
				} else {
444 3
					$id      = 0;
445 3
					$options = wc_get_text_attributes( $meta_value['value'] );
446
				}
447
448 3
				$attribute = new WC_Product_Attribute();
449 3
				$attribute->set_id( $id );
450 3
				$attribute->set_name( $meta_value['name'] );
451 3
				$attribute->set_options( $options );
452 3
				$attribute->set_position( $meta_value['position'] );
453 3
				$attribute->set_visible( $meta_value['is_visible'] );
0 ignored issues
show
Documentation introduced by
$meta_value['is_visible'] is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
454 3
				$attribute->set_variation( $meta_value['is_variation'] );
0 ignored issues
show
Documentation introduced by
$meta_value['is_variation'] is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
455 3
				$attributes[] = $attribute;
456
			}
457 3
			$product->set_attributes( $attributes );
458
		}
459
	}
460
461
	/**
462
	 * Read downloads from post meta.
463
	 *
464
	 * @param WC_Product $product Product object.
465
	 * @since 3.0.0
466
	 */
467 374
	protected function read_downloads( &$product ) {
468 374
		$meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );
469
470 374
		if ( $meta_values ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $meta_values of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
471 3
			$downloads = array();
472 3
			foreach ( $meta_values as $key => $value ) {
473 3
				if ( ! isset( $value['name'], $value['file'] ) ) {
474
					continue;
475
				}
476 3
				$download = new WC_Product_Download();
477 3
				$download->set_id( $key );
478 3
				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
479 3
				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
480 3
				$downloads[] = $download;
481
			}
482 3
			$product->set_downloads( $downloads );
483
		}
484
	}
485
486
	/**
487
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
488
	 *
489
	 * @param WC_Product $product Product object.
490
	 * @param bool       $force Force update. Used during create.
491
	 * @since 3.0.0
492
	 */
493 399
	protected function update_post_meta( &$product, $force = false ) {
494
		$meta_key_to_props = array(
495 399
			'_sku'                   => 'sku',
496
			'_regular_price'         => 'regular_price',
497
			'_sale_price'            => 'sale_price',
498
			'_sale_price_dates_from' => 'date_on_sale_from',
499
			'_sale_price_dates_to'   => 'date_on_sale_to',
500
			'total_sales'            => 'total_sales',
501
			'_tax_status'            => 'tax_status',
502
			'_tax_class'             => 'tax_class',
503
			'_manage_stock'          => 'manage_stock',
504
			'_backorders'            => 'backorders',
505
			'_low_stock_amount'      => 'low_stock_amount',
506
			'_sold_individually'     => 'sold_individually',
507
			'_weight'                => 'weight',
508
			'_length'                => 'length',
509
			'_width'                 => 'width',
510
			'_height'                => 'height',
511
			'_upsell_ids'            => 'upsell_ids',
512
			'_crosssell_ids'         => 'cross_sell_ids',
513
			'_purchase_note'         => 'purchase_note',
514
			'_default_attributes'    => 'default_attributes',
515
			'_virtual'               => 'virtual',
516
			'_downloadable'          => 'downloadable',
517
			'_product_image_gallery' => 'gallery_image_ids',
518
			'_download_limit'        => 'download_limit',
519
			'_download_expiry'       => 'download_expiry',
520
			'_thumbnail_id'          => 'image_id',
521
			'_stock'                 => 'stock_quantity',
522
			'_stock_status'          => 'stock_status',
523
			'_wc_average_rating'     => 'average_rating',
524
			'_wc_rating_count'       => 'rating_counts',
525
			'_wc_review_count'       => 'review_count',
526
		);
527
528
		// Make sure to take extra data (like product url or text for external products) into account.
529 399
		$extra_data_keys = $product->get_extra_data_keys();
530
531 399
		foreach ( $extra_data_keys as $key ) {
532 23
			$meta_key_to_props[ '_' . $key ] = $key;
533
		}
534
535 399
		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
536
537 399
		foreach ( $props_to_update as $meta_key => $prop ) {
538 399
			$value = $product->{"get_$prop"}( 'edit' );
539 399
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
540 399
			switch ( $prop ) {
541
				case 'virtual':
542
				case 'downloadable':
543
				case 'manage_stock':
544
				case 'sold_individually':
545 399
					$value = wc_bool_to_string( $value );
546 399
					break;
547
				case 'gallery_image_ids':
548 399
					$value = implode( ',', $value );
549 399
					break;
550
				case 'date_on_sale_from':
551
				case 'date_on_sale_to':
552 399
					$value = $value ? $value->getTimestamp() : '';
553 399
					break;
554
			}
555
556 399
			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
557
558 399
			if ( $updated ) {
559 399
				$this->updated_props[] = $prop;
560
			}
561
		}
562
563
		// Update extra data associated with the product like button text or product URL for external products.
564 399
		if ( ! $this->extra_data_saved ) {
565 399
			foreach ( $extra_data_keys as $key ) {
566 23
				$meta_key = '_' . $key;
567 23
				$function = 'get_' . $key;
568 23
				if ( ! array_key_exists( $meta_key, $props_to_update ) ) {
569 1
					continue;
570
				}
571 23
				if ( is_callable( array( $product, $function ) ) ) {
572 23
					$value   = $product->{$function}( 'edit' );
573 23
					$value   = is_string( $value ) ? wp_slash( $value ) : $value;
574 23
					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
575
576 23
					if ( $updated ) {
577
						$this->updated_props[] = $key;
578
					}
579
				}
580
			}
581
		}
582
583 399
		if ( $this->update_downloads( $product, $force ) ) {
584 3
			$this->updated_props[] = 'downloads';
585
		}
586
	}
587
588
	/**
589
	 * Handle updated meta props after updating meta data.
590
	 *
591
	 * @since 3.0.0
592
	 * @param WC_Product $product Product Object.
593
	 */
594 399
	protected function handle_updated_props( &$product ) {
595 399
		$price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );
596
597 399
		if ( ! $price_is_synced ) {
598 396
			if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
599 355
				if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
600
					update_post_meta( $product->get_id(), '_sale_price', '' );
601
					$product->set_sale_price( '' );
602
				}
603
			}
604
605 396
			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 ) ) {
606 356
				if ( $product->is_on_sale( 'edit' ) ) {
607 16
					update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
608 16
					$product->set_price( $product->get_sale_price( 'edit' ) );
609
				} else {
610 355
					update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
611 355
					$product->set_price( $product->get_regular_price( 'edit' ) );
612
				}
613
			}
614
		}
615
616 399
		if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
617 399
			if ( $product->is_type( 'variation' ) ) {
618 63
				do_action( 'woocommerce_variation_set_stock', $product );
619
			} else {
620 398
				do_action( 'woocommerce_product_set_stock', $product );
621
			}
622
		}
623
624 399
		if ( in_array( 'stock_status', $this->updated_props, true ) ) {
625 399
			if ( $product->is_type( 'variation' ) ) {
626 63
				do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
627
			} else {
628 398
				do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
629
			}
630
		}
631
632 399
		if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual' ) ) ) {
633 399
			$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
634
		}
635
636
		// Trigger action so 3rd parties can deal with updated props.
637 399
		do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );
638
639
		// After handling, we can reset the props array.
640 399
		$this->updated_props = array();
641
	}
642
643
	/**
644
	 * For all stored terms in all taxonomies, save them to the DB.
645
	 *
646
	 * @param WC_Product $product Product object.
647
	 * @param bool       $force Force update. Used during create.
648
	 * @since 3.0.0
649
	 */
650 398
	protected function update_terms( &$product, $force = false ) {
651 398
		$changes = $product->get_changes();
652
653 398
		if ( $force || array_key_exists( 'category_ids', $changes ) ) {
654 398
			$categories = $product->get_category_ids( 'edit' );
655
656 398
			if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
657 389
				$categories = array( get_option( 'default_product_cat', 0 ) );
658
			}
659
660 398
			wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
661
		}
662 398
		if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
663 398
			wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
664
		}
665 398 View Code Duplication
		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
666 398
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
667
		}
668
	}
669
670
	/**
671
	 * Update visibility terms based on props.
672
	 *
673
	 * @since 3.0.0
674
	 *
675
	 * @param WC_Product $product Product object.
676
	 * @param bool       $force Force update. Used during create.
677
	 */
678 398
	protected function update_visibility( &$product, $force = false ) {
679 398
		$changes = $product->get_changes();
680
681 398
		if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
682 398
			$terms = array();
683
684 398
			if ( $product->get_featured() ) {
685 4
				$terms[] = 'featured';
686
			}
687
688 398
			if ( 'outofstock' === $product->get_stock_status() ) {
689 68
				$terms[] = 'outofstock';
690
			}
691
692 398
			$rating = min( 5, round( $product->get_average_rating(), 0 ) );
693
694 398
			if ( $rating > 0 ) {
695 1
				$terms[] = 'rated-' . $rating;
696
			}
697
698 398
			switch ( $product->get_catalog_visibility() ) {
699
				case 'hidden':
700 2
					$terms[] = 'exclude-from-search';
701 2
					$terms[] = 'exclude-from-catalog';
702 2
					break;
703
				case 'catalog':
704 1
					$terms[] = 'exclude-from-search';
705 1
					break;
706
				case 'search':
707 3
					$terms[] = 'exclude-from-catalog';
708 3
					break;
709
			}
710
711 398
			if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
712 398
				do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
713
			}
714
		}
715
	}
716
717
	/**
718
	 * Update attributes which are a mix of terms and meta data.
719
	 *
720
	 * @param WC_Product $product Product object.
721
	 * @param bool       $force Force update. Used during create.
722
	 * @since 3.0.0
723
	 */
724 398
	protected function update_attributes( &$product, $force = false ) {
725 398
		$changes = $product->get_changes();
726
727 398
		if ( $force || array_key_exists( 'attributes', $changes ) ) {
728 398
			$attributes  = $product->get_attributes();
729 398
			$meta_values = array();
730
731 398
			if ( $attributes ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attributes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
732 56
				foreach ( $attributes as $attribute_key => $attribute ) {
733 56
					$value = '';
734
735 56
					if ( is_null( $attribute ) ) {
736
						if ( taxonomy_exists( $attribute_key ) ) {
737
							// Handle attributes that have been unset.
738
							wp_set_object_terms( $product->get_id(), array(), $attribute_key );
739
						} elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) {
740
							// Handle attributes that have been unset.
741
							wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) );
742
						}
743
						continue;
744
745 56
					} elseif ( $attribute->is_taxonomy() ) {
746 46
						wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
747
					} else {
748 13
						$value = wc_implode_text_attributes( $attribute->get_options() );
749
					}
750
751
					// Store in format WC uses in meta.
752 56
					$meta_values[ $attribute_key ] = array(
753 56
						'name'         => $attribute->get_name(),
754 56
						'value'        => $value,
755 56
						'position'     => $attribute->get_position(),
756 56
						'is_visible'   => $attribute->get_visible() ? 1 : 0,
757 56
						'is_variation' => $attribute->get_variation() ? 1 : 0,
758 56
						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
759
					);
760
				}
761
			}
762
			// Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround.
763 398
			$this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) );
764
		}
765
	}
766
767
	/**
768
	 * Update downloads.
769
	 *
770
	 * @since 3.0.0
771
	 * @param WC_Product $product Product object.
772
	 * @param bool       $force Force update. Used during create.
773
	 * @return bool If updated or not.
774
	 */
775 399
	protected function update_downloads( &$product, $force = false ) {
776 399
		$changes = $product->get_changes();
777
778 399
		if ( $force || array_key_exists( 'downloads', $changes ) ) {
779 399
			$downloads   = $product->get_downloads();
780 399
			$meta_values = array();
781
782 399
			if ( $downloads ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $downloads of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
783 3
				foreach ( $downloads as $key => $download ) {
784
					// Store in format WC uses in meta.
785 3
					$meta_values[ $key ] = $download->get_data();
786
				}
787
			}
788
789 399
			if ( $product->is_type( 'variation' ) ) {
790 63
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
791
			} else {
792 398
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
793
			}
794
795 399
			return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) );
796
		}
797 119
		return false;
798
	}
799
800
	/**
801
	 * Make sure we store the product type and version (to track data changes).
802
	 *
803
	 * @param WC_Product $product Product object.
804
	 * @since 3.0.0
805
	 */
806 398
	protected function update_version_and_type( &$product ) {
807 398
		$old_type = WC_Product_Factory::get_product_type( $product->get_id() );
808 398
		$new_type = $product->get_type();
809
810 398
		wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
811 398
		update_post_meta( $product->get_id(), '_product_version', WC_VERSION );
812
813
		// Action for the transition.
814 398
		if ( $old_type !== $new_type ) {
815 76
			$this->updated_props[] = 'product_type';
816 76
			do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
817
		}
818
	}
819
820
	/**
821
	 * Clear any caches.
822
	 *
823
	 * @param WC_Product $product Product object.
824
	 * @since 3.0.0
825
	 */
826 399
	protected function clear_caches( &$product ) {
827 399
		wc_delete_product_transients( $product->get_id() );
828 399
		if ( $product->get_parent_id( 'edit' ) ) {
829 61
			wc_delete_product_transients( $product->get_parent_id( 'edit' ) );
830 61
			WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_parent_id( 'edit' ) );
831
		}
832 399
		WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) );
833 399
		WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_id() );
834
	}
835
836
	/*
837
	|--------------------------------------------------------------------------
838
	| wc-product-functions.php methods
839
	|--------------------------------------------------------------------------
840
	*/
841
842
	/**
843
	 * Returns an array of on sale products, as an array of objects with an
844
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
845
	 *
846
	 * @return array
847
	 * @since 3.0.0
848
	 */
849 4
	public function get_on_sale_products() {
850
		global $wpdb;
851
852 4
		$exclude_term_ids            = array();
853 4
		$outofstock_join             = '';
854 4
		$outofstock_where            = '';
855 4
		$non_published_where         = '';
856 4
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
857
858 4 View Code Duplication
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
859
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
860
		}
861
862 4
		if ( count( $exclude_term_ids ) ) {
863
			$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';
864
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
865
		}
866
867
		// Fetch a list of non-published parent products and exlude them, quicker than joining in the main query below.
868 4
		$non_published_products = $wpdb->get_col(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
869
			"
870 4
			SELECT posts.ID as id FROM `$wpdb->posts` AS posts
871
			WHERE posts.post_type = 'product'
872
			AND posts.post_parent = 0
873
			AND posts.post_status != 'publish'
874
			"
875
		);
876 4
		if ( 0 < count( $non_published_products ) ) {
877 1
			$non_published_where = ' AND posts.post_parent NOT IN ( ' . implode( ',', $non_published_products ) . ')';
878
		}
879
880 4
		return $wpdb->get_results(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
881
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
882
			"
883
			SELECT posts.ID as id, posts.post_parent as parent_id
884 4
			FROM {$wpdb->posts} AS posts
885 4
			INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
886 4
			$outofstock_join
887
			WHERE posts.post_type IN ( 'product', 'product_variation' )
888
			AND posts.post_status = 'publish'
889
			AND lookup.onsale = 1
890 4
			$outofstock_where
891 4
			$non_published_where
892
			GROUP BY posts.ID
893
			"
894
			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
895
		);
896
	}
897
898
	/**
899
	 * Returns a list of product IDs ( id as key => parent as value) that are
900
	 * featured. Uses get_posts instead of wc_get_products since we want
901
	 * some extra meta queries and ALL products (posts_per_page = -1).
902
	 *
903
	 * @return array
904
	 * @since 3.0.0
905
	 */
906 2
	public function get_featured_product_ids() {
907 2
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
908
909 2
		return get_posts(
910
			array(
911 2
				'post_type'      => array( 'product', 'product_variation' ),
912
				'posts_per_page' => -1,
0 ignored issues
show
introduced by
Disabling pagination is prohibited in VIP context, do not set posts_per_page to -1 ever.
Loading history...
913 2
				'post_status'    => 'publish',
914
				'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
0 ignored issues
show
introduced by
Detected usage of tax_query, possible slow query.
Loading history...
915 2
					'relation' => 'AND',
916
					array(
917 2
						'taxonomy' => 'product_visibility',
918 2
						'field'    => 'term_taxonomy_id',
919 2
						'terms'    => array( $product_visibility_term_ids['featured'] ),
920
					),
921
					array(
922 2
						'taxonomy' => 'product_visibility',
923 2
						'field'    => 'term_taxonomy_id',
924 2
						'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
925 2
						'operator' => 'NOT IN',
926
					),
927
				),
928 2
				'fields'         => 'id=>parent',
929
			)
930
		);
931
	}
932
933
	/**
934
	 * Check if product sku is found for any other product IDs.
935
	 *
936
	 * @since 3.0.0
937
	 * @param int    $product_id Product ID.
938
	 * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
939
	 * @return bool
940
	 */
941 341 View Code Duplication
	public function is_existing_sku( $product_id, $sku ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
942
		global $wpdb;
943
944
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
945 341
		return $wpdb->get_var(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
946 341
			$wpdb->prepare(
947 341
				"
948
				SELECT posts.ID
949 341
				FROM {$wpdb->posts} as posts
950 341
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
951
				WHERE
952
				posts.post_type IN ( 'product', 'product_variation' )
953
				AND posts.post_status != 'trash'
954
				AND lookup.sku = %s
955
				AND lookup.product_id <> %d
956
				LIMIT 1
957
				",
958 341
				wp_slash( $sku ),
959
				$product_id
960
			)
961
		);
962
	}
963
964
	/**
965
	 * Return product ID based on SKU.
966
	 *
967
	 * @since 3.0.0
968
	 * @param string $sku Product SKU.
969
	 * @return int
970
	 */
971 94 View Code Duplication
	public function get_product_id_by_sku( $sku ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
972
		global $wpdb;
973
974
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
975 94
		$id = $wpdb->get_var(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
976 94
			$wpdb->prepare(
977 94
				"
978
				SELECT posts.ID
979 94
				FROM {$wpdb->posts} as posts
980 94
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
981
				WHERE
982
				posts.post_type IN ( 'product', 'product_variation' )
983
				AND posts.post_status != 'trash'
984
				AND lookup.sku = %s
985
				LIMIT 1
986
				",
987
				$sku
988
			)
989
		);
990
991 94
		return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
992
	}
993
994
	/**
995
	 * Returns an array of IDs of products that have sales starting soon.
996
	 *
997
	 * @since 3.0.0
998
	 * @return array
999
	 */
1000
	public function get_starting_sales() {
1001
		global $wpdb;
1002
1003
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1004
		return $wpdb->get_col(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1005
			$wpdb->prepare(
1006
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1007
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1008
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1009
				WHERE postmeta.meta_key = '_sale_price_dates_from'
1010
					AND postmeta_2.meta_key = '_price'
1011
					AND postmeta_3.meta_key = '_sale_price'
1012
					AND postmeta.meta_value > 0
1013
					AND postmeta.meta_value < %s
1014
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1015
				current_time( 'timestamp', true )
1016
			)
1017
		);
1018
	}
1019
1020
	/**
1021
	 * Returns an array of IDs of products that have sales which are due to end.
1022
	 *
1023
	 * @since 3.0.0
1024
	 * @return array
1025
	 */
1026
	public function get_ending_sales() {
1027
		global $wpdb;
1028
1029
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1030
		return $wpdb->get_col(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1031
			$wpdb->prepare(
1032
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1033
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1034
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1035
				WHERE postmeta.meta_key = '_sale_price_dates_to'
1036
					AND postmeta_2.meta_key = '_price'
1037
					AND postmeta_3.meta_key = '_regular_price'
1038
					AND postmeta.meta_value > 0
1039
					AND postmeta.meta_value < %s
1040
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1041
				current_time( 'timestamp', true )
1042
			)
1043
		);
1044
	}
1045
1046
	/**
1047
	 * Find a matching (enabled) variation within a variable product.
1048
	 *
1049
	 * @since  3.0.0
1050
	 * @param  WC_Product $product Variable product.
1051
	 * @param  array      $match_attributes Array of attributes we want to try to match.
1052
	 * @return int Matching variation ID or 0.
1053
	 */
1054
	public function find_matching_product_variation( $product, $match_attributes = array() ) {
1055
		global $wpdb;
1056
1057
		$meta_attribute_names = array();
1058
1059
		// Get attributes to match in meta.
1060
		foreach ( $product->get_attributes() as $attribute ) {
1061
			if ( ! $attribute->get_variation() ) {
1062
				continue;
1063
			}
1064
1065
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() );
1066
1067
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
1068
				return 0;
1069
			}
1070
1071
			$meta_attribute_names[] = $attribute_field_name;
1072
		}
1073
1074
		// Get the attributes of the variations.
1075
		$query = $wpdb->prepare(
1076
			"
1077
			SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta}
1078
			WHERE post_id IN (
1079
				SELECT ID FROM {$wpdb->posts}
1080
				WHERE {$wpdb->posts}.post_parent = %d
1081
				AND {$wpdb->posts}.post_status = 'publish'
1082
				AND {$wpdb->posts}.post_type = 'product_variation'
1083
				ORDER BY menu_order ASC, ID ASC
1084
			)
1085
			",
1086
			$product->get_id()
1087
		);
1088
1089
		$query .= ' AND meta_key IN ( "' . implode( '","', array_map( 'esc_sql', $meta_attribute_names ) ) . '" );';
1090
1091
		$attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1092
1093
		if ( ! $attributes ) {
1094
			return 0;
1095
		}
1096
1097
		$sorted_meta = array();
1098
1099
		foreach ( $attributes as $m ) {
1100
			$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
introduced by
Detected usage of meta_key, possible slow query.
Loading history...
1101
		}
1102
1103
		/**
1104
		 * Check each variation to find the one that matches the $match_attributes.
1105
		 *
1106
		 * Note: Not all meta fields will be set which is why we check existance.
1107
		 */
1108
		foreach ( $sorted_meta as $variation_id => $variation ) {
1109
			$match = true;
1110
1111
			foreach ( $match_attributes as $attribute_key => $attribute_value ) {
1112
				if ( array_key_exists( $attribute_key, $variation ) ) {
1113
					if ( $variation[ $attribute_key ] !== $attribute_value && ! empty( $variation[ $attribute_key ] ) ) {
1114
						$match = false;
1115
					}
1116
				}
1117
			}
1118
1119
			if ( true === $match ) {
1120
				return $variation_id;
1121
			}
1122
		}
1123
1124
		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
1125
			/**
1126
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
1127
			 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
1128
			 */
1129
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
1130
		}
1131
	}
1132
1133
	/**
1134
	 * Creates all possible combinations of variations from the attributes, without creating duplicates.
1135
	 *
1136
	 * @since  3.6.0
1137
	 * @todo   Add to interface in 4.0.
1138
	 * @param  WC_Product $product Variable product.
1139
	 * @param  int        $limit Limit the number of created variations.
1140
	 * @return int        Number of created variations.
1141
	 */
1142 2
	public function create_all_product_variations( $product, $limit = -1 ) {
1143 2
		$count = 0;
1144
1145 2
		if ( ! $product ) {
1146
			return $count;
1147
		}
1148
1149 2
		$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );
1150
1151 2
		if ( empty( $attributes ) ) {
1152
			return $count;
1153
		}
1154
1155
		// Get existing variations so we don't create duplicates.
1156 2
		$existing_variations = array_map( 'wc_get_product', $product->get_children() );
1157 2
		$existing_attributes = array();
1158
1159 2
		foreach ( $existing_variations as $existing_variation ) {
1160 2
			$existing_attributes[] = $existing_variation->get_attributes();
1161
		}
1162
1163 2
		$possible_attributes = array_reverse( wc_array_cartesian( $attributes ) );
1164
1165 2
		foreach ( $possible_attributes as $possible_attribute ) {
1166
			// Allow any order if key/values -- do not use strict mode.
1167 2
			if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1168 2
				continue;
1169
			}
1170 2
			$variation = new WC_Product_Variation();
1171 2
			$variation->set_parent_id( $product->get_id() );
1172 2
			$variation->set_attributes( $possible_attribute );
1173 2
			$variation_id = $variation->save();
1174
1175 2
			do_action( 'product_variation_linked', $variation_id );
1176
1177 2
			$count ++;
1178
1179 2
			if ( $limit > 0 && $count >= $limit ) {
1180 1
				break;
1181
			}
1182
		}
1183
1184 2
		return $count;
1185
	}
1186
1187
	/**
1188
	 * Make sure all variations have a sort order set so they can be reordered correctly.
1189
	 *
1190
	 * @param int $parent_id Product ID.
1191
	 */
1192
	public function sort_all_product_variations( $parent_id ) {
1193
		global $wpdb;
1194
1195
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1196
		$ids   = $wpdb->get_col(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1197
			$wpdb->prepare(
1198
				"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",
1199
				$parent_id
1200
			)
1201
		);
1202
		$index = 1;
1203
1204
		foreach ( $ids as $id ) {
1205
			// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1206
			$wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
1207
		}
1208
	}
1209
1210
	/**
1211
	 * Return a list of related products (using data like categories and IDs).
1212
	 *
1213
	 * @since 3.0.0
1214
	 * @param array $cats_array  List of categories IDs.
1215
	 * @param array $tags_array  List of tags IDs.
1216
	 * @param array $exclude_ids Excluded IDs.
1217
	 * @param int   $limit       Limit of results.
1218
	 * @param int   $product_id  Product ID.
1219
	 * @return array
1220
	 */
1221 27
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
1222
		global $wpdb;
1223
1224
		$args = array(
1225 27
			'categories'  => $cats_array,
1226 27
			'tags'        => $tags_array,
1227 27
			'exclude_ids' => $exclude_ids,
1228 27
			'limit'       => $limit + 10,
1229
		);
1230
1231 27
		$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 );
1232
1233
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
1234 27
		return $wpdb->get_col( implode( ' ', $related_product_query ) );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1235
	}
1236
1237
	/**
1238
	 * Builds the related posts query.
1239
	 *
1240
	 * @since 3.0.0
1241
	 *
1242
	 * @param array $cats_array  List of categories IDs.
1243
	 * @param array $tags_array  List of tags IDs.
1244
	 * @param array $exclude_ids Excluded IDs.
1245
	 * @param int   $limit       Limit of results.
1246
	 *
1247
	 * @return array
1248
	 */
1249 27
	public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
1250
		global $wpdb;
1251
1252 27
		$include_term_ids            = array_merge( $cats_array, $tags_array );
1253 27
		$exclude_term_ids            = array();
1254 27
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1255
1256 27
		if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
1257 27
			$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
1258
		}
1259
1260 27 View Code Duplication
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
1261
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
1262
		}
1263
1264
		$query = array(
1265
			'fields' => "
1266 27
				SELECT DISTINCT ID FROM {$wpdb->posts} p
1267
			",
1268 27
			'join'   => '',
1269 27
			'where'  => "
1270
				WHERE 1=1
1271
				AND p.post_status = 'publish'
1272
				AND p.post_type = 'product'
1273
1274
			",
1275
			'limits' => '
0 ignored issues
show
introduced by
Each line in an array declaration must end in a comma
Loading history...
1276 27
				LIMIT ' . absint( $limit ) . '
1277
			',
1278
		);
1279
1280 27 View Code Duplication
		if ( count( $exclude_term_ids ) ) {
1281 27
			$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';
1282 27
			$query['where'] .= ' AND exclude_join.object_id IS NULL';
1283
		}
1284
1285 27 View Code Duplication
		if ( count( $include_term_ids ) ) {
1286 27
			$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';
1287
		}
1288
1289 27
		if ( count( $exclude_ids ) ) {
1290 27
			$query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
1291
		}
1292
1293 27
		return $query;
1294
	}
1295
1296
	/**
1297
	 * Update a product's stock amount directly.
1298
	 *
1299
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
1300
	 *
1301
	 * @since  3.0.0 this supports set, increase and decrease.
1302
	 * @param  int      $product_id_with_stock Product ID.
1303
	 * @param  int|null $stock_quantity Stock quantity.
1304
	 * @param  string   $operation Set, increase and decrease.
1305
	 */
1306 2
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
1307
		global $wpdb;
1308 2
		add_post_meta( $product_id_with_stock, '_stock', 0, true );
1309
1310
		// Update stock in DB directly.
1311 2
		switch ( $operation ) {
1312
			case 'increase':
1313 1
				$sql = $wpdb->prepare(
1314 1
					"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'",
1315
					$stock_quantity,
1316
					$product_id_with_stock
1317
				);
1318 1
				break;
1319
			case 'decrease':
1320 1
				$sql = $wpdb->prepare(
1321 1
					"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'",
1322
					$stock_quantity,
1323
					$product_id_with_stock
1324
				);
1325 1
				break;
1326
			default:
1327 2
				$sql = $wpdb->prepare(
1328 2
					"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
1329
					$stock_quantity,
1330
					$product_id_with_stock
1331
				);
1332 2
				break;
1333
		}
1334
1335 2
		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, $operation );
1336
1337
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
1338 2
		$wpdb->query( $sql );
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1339
1340 2
		wp_cache_delete( $product_id_with_stock, 'post_meta' );
1341
1342 2
		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );
1343
1344
		/**
1345
		 * Fire an action for this direct update so it can be detected by other code.
1346
		 *
1347
		 * @since 3.6
1348
		 * @param int $product_id_with_stock Product ID that was updated directly.
1349
		 */
1350 2
		do_action( 'woocommerce_updated_product_stock', $product_id_with_stock );
1351
	}
1352
1353
	/**
1354
	 * Update a product's sale count directly.
1355
	 *
1356
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
1357
	 *
1358
	 * @since  3.0.0 this supports set, increase and decrease.
1359
	 * @param  int      $product_id Product ID.
1360
	 * @param  int|null $quantity Quantity.
1361
	 * @param  string   $operation set, increase and decrease.
1362
	 */
1363 13
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
1364
		global $wpdb;
1365 13
		add_post_meta( $product_id, 'total_sales', 0, true );
1366
1367
		// Update stock in DB directly.
1368 13
		switch ( $operation ) {
1369
			case 'increase':
1370
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1371 13
				$wpdb->query(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1372 13
					$wpdb->prepare(
1373 13
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'",
1374
						$quantity,
1375
						$product_id
1376
					)
1377
				);
1378 13
				break;
1379
			case 'decrease':
1380
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1381
				$wpdb->query(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1382
					$wpdb->prepare(
1383
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'",
1384
						$quantity,
1385
						$product_id
1386
					)
1387
				);
1388
				break;
1389
			default:
1390
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1391
				$wpdb->query(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1392
					$wpdb->prepare(
1393
						"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'",
1394
						$quantity,
1395
						$product_id
1396
					)
1397
				);
1398
				break;
1399
		}
1400
1401 13
		wp_cache_delete( $product_id, 'post_meta' );
1402
1403 13
		$this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );
1404
1405
		/**
1406
		 * Fire an action for this direct update so it can be detected by other code.
1407
		 *
1408
		 * @since 3.6
1409
		 * @param int $product_id Product ID that was updated directly.
1410
		 */
1411 13
		do_action( 'woocommerce_updated_product_sales', $product_id );
1412
	}
1413
1414
	/**
1415
	 * Update a products average rating meta.
1416
	 *
1417
	 * @since 3.0.0
1418
	 * @todo Deprecate unused function?
1419
	 * @param WC_Product $product Product object.
1420
	 */
1421
	public function update_average_rating( $product ) {
1422
		update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
1423
		self::update_visibility( $product, true );
1424
	}
1425
1426
	/**
1427
	 * Update a products review count meta.
1428
	 *
1429
	 * @since 3.0.0
1430
	 * @todo Deprecate unused function?
1431
	 * @param WC_Product $product Product object.
1432
	 */
1433
	public function update_review_count( $product ) {
1434
		update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
1435
	}
1436
1437
	/**
1438
	 * Update a products rating counts.
1439
	 *
1440
	 * @since 3.0.0
1441
	 * @todo Deprecate unused function?
1442
	 * @param WC_Product $product Product object.
1443
	 */
1444
	public function update_rating_counts( $product ) {
1445
		update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
1446
	}
1447
1448
	/**
1449
	 * Get shipping class ID by slug.
1450
	 *
1451
	 * @since 3.0.0
1452
	 * @param string $slug Product shipping class slug.
1453
	 * @return int|false
1454
	 */
1455 2
	public function get_shipping_class_id_by_slug( $slug ) {
1456 2
		$shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
1457 2
		if ( $shipping_class_term ) {
1458 2
			return $shipping_class_term->term_id;
1459
		} else {
1460
			return false;
1461
		}
1462
	}
1463
1464
	/**
1465
	 * Returns an array of products.
1466
	 *
1467
	 * @param  array $args Args to pass to WC_Product_Query().
1468
	 * @return array|object
1469
	 * @see wc_get_products
1470
	 */
1471
	public function get_products( $args = array() ) {
1472
		$query = new WC_Product_Query( $args );
1473
		return $query->get_products();
1474
	}
1475
1476
	/**
1477
	 * Search product data for a term and return ids.
1478
	 *
1479
	 * @param  string     $term Search term.
1480
	 * @param  string     $type Type of product.
1481
	 * @param  bool       $include_variations Include variations in search or not.
1482
	 * @param  bool       $all_statuses Should we search all statuses or limit to published.
1483
	 * @param  null|int   $limit Limit returned results. @since 3.5.0.
1484
	 * @param  null|array $include Keep specific results. @since 3.6.0.
1485
	 * @param  null|array $exclude Discard specific results. @since 3.6.0.
1486
	 * @return array of ids
1487
	 */
1488 1
	public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) {
1489
		global $wpdb;
1490
1491 1
		$custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit );
1492
1493 1
		if ( is_array( $custom_results ) ) {
1494
			return $custom_results;
1495
		}
1496
1497 1
		$post_types    = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
1498 1
		$post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' );
1499 1
		$type_where    = '';
1500 1
		$status_where  = '';
1501 1
		$limit_query   = '';
1502 1
		$term          = wc_strtolower( $term );
1503
1504
		// See if search term contains OR keywords.
1505 1
		if ( strstr( $term, ' or ' ) ) {
1506 1
			$term_groups = explode( ' or ', $term );
1507
		} else {
1508 1
			$term_groups = array( $term );
1509
		}
1510
1511 1
		$search_where   = '';
1512 1
		$search_queries = array();
1513
1514 1
		foreach ( $term_groups as $term_group ) {
1515
			// Parse search terms.
1516 1
			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
1517 1
				$search_terms = $this->get_valid_search_terms( $matches[0] );
1518 1
				$count        = count( $search_terms );
1519
1520
				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
1521 1
				if ( 9 < $count || 0 === $count ) {
1522 1
					$search_terms = array( $term_group );
1523
				}
1524
			} else {
1525
				$search_terms = array( $term_group );
1526
			}
1527
1528 1
			$term_group_query = '';
1529 1
			$searchand        = '';
1530
1531 1
			foreach ( $search_terms as $search_term ) {
1532 1
				$like              = '%' . $wpdb->esc_like( $search_term ) . '%';
1533 1
				$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
1534 1
				$searchand         = ' AND ';
1535
			}
1536
1537 1
			if ( $term_group_query ) {
1538 1
				$search_queries[] = $term_group_query;
1539
			}
1540
		}
1541
1542 1
		if ( ! empty( $search_queries ) ) {
1543 1
			$search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') ';
1544
		}
1545
1546 1 View Code Duplication
		if ( ! empty( $include ) && is_array( $include ) ) {
1547 1
			$search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') ';
1548
		}
1549
1550 1 View Code Duplication
		if ( ! empty( $exclude ) && is_array( $exclude ) ) {
1551 1
			$search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') ';
1552
		}
1553
1554 1
		if ( 'virtual' === $type ) {
1555
			$type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) ';
1556 1
		} elseif ( 'downloadable' === $type ) {
1557
			$type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) ';
1558
		}
1559
1560 1
		if ( ! $all_statuses ) {
1561
			$status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
1562
		}
1563
1564 1
		if ( $limit ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1565 1
			$limit_query = $wpdb->prepare( ' LIMIT %d ', $limit );
1566
		}
1567
1568
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1569 1
		$search_results = $wpdb->get_results(
0 ignored issues
show
introduced by
Usage of a direct database call is discouraged.
Loading history...
introduced by
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1570
			// phpcs:disable
1571 1
			"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
1572 1
			 LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
1573 1
			WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
1574 1
			$search_where
1575 1
			$status_where
1576 1
			$type_where
1577
			ORDER BY posts.post_parent ASC, posts.post_title ASC
1578 1
			$limit_query
1579
			"
1580
			// phpcs:enable
1581
		);
1582
1583 1
		$product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );
1584
1585 1
		if ( is_numeric( $term ) ) {
1586
			$post_id   = absint( $term );
1587
			$post_type = get_post_type( $post_id );
1588
1589
			if ( 'product_variation' === $post_type && $include_variations ) {
1590
				$product_ids[] = $post_id;
1591
			} elseif ( 'product' === $post_type ) {
1592
				$product_ids[] = $post_id;
1593
			}
1594
1595
			$product_ids[] = wp_get_post_parent_id( $post_id );
1596
		}
1597
1598 1
		return wp_parse_id_list( $product_ids );
1599
	}
1600
1601
	/**
1602
	 * Get the product type based on product ID.
1603
	 *
1604
	 * @since 3.0.0
1605
	 * @param int $product_id Product ID.
1606
	 * @return bool|string
1607
	 */
1608 401
	public function get_product_type( $product_id ) {
1609 401
		$cache_key    = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
1610 401
		$product_type = wp_cache_get( $cache_key, 'products' );
1611
1612 401
		if ( $product_type ) {
1613 229
			return $product_type;
1614
		}
1615
1616 401
		$post_type = get_post_type( $product_id );
1617
1618 401
		if ( 'product_variation' === $post_type ) {
1619 63
			$product_type = 'variation';
1620 400
		} elseif ( 'product' === $post_type ) {
1621 398
			$terms        = get_the_terms( $product_id, 'product_type' );
1622 398
			$product_type = ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
1623
		} else {
1624 2
			$product_type = false;
1625
		}
1626
1627 401
		wp_cache_set( $cache_key, $product_type, 'products' );
1628
1629 401
		return $product_type;
1630
	}
1631
1632
	/**
1633
	 * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
1634
	 *
1635
	 * @since 3.2.0
1636
	 * @param string   $where Where clause.
1637
	 * @param WP_Query $wp_query WP_Query instance.
1638
	 * @return string
1639
	 */
1640 1
	public function reviews_allowed_query_where( $where, $wp_query ) {
1641
		global $wpdb;
1642
1643 1
		if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
1644 1
			if ( $wp_query->query_vars['reviews_allowed'] ) {
1645 1
				$where .= " AND $wpdb->posts.comment_status = 'open'";
1646
			} else {
1647 1
				$where .= " AND $wpdb->posts.comment_status = 'closed'";
1648
			}
1649
		}
1650
1651 1
		return $where;
1652
	}
1653
1654
	/**
1655
	 * Get valid WP_Query args from a WC_Product_Query's query variables.
1656
	 *
1657
	 * @since 3.2.0
1658
	 * @param array $query_vars Query vars from a WC_Product_Query.
1659
	 * @return array
1660
	 */
1661 14
	protected function get_wp_query_args( $query_vars ) {
1662
1663
		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
1664
		$key_mapping = array(
1665 14
			'status'         => 'post_status',
1666
			'page'           => 'paged',
1667
			'include'        => 'post__in',
1668
			'stock_quantity' => 'stock',
1669
			'average_rating' => 'wc_average_rating',
1670
			'review_count'   => 'wc_review_count',
1671
		);
1672 14 View Code Duplication
		foreach ( $key_mapping as $query_key => $db_key ) {
1673 14
			if ( isset( $query_vars[ $query_key ] ) ) {
1674 14
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
1675 14
				unset( $query_vars[ $query_key ] );
1676
			}
1677
		}
1678
1679
		// Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
1680
		$boolean_queries = array(
1681 14
			'virtual',
1682
			'downloadable',
1683
			'sold_individually',
1684
			'manage_stock',
1685
		);
1686 14
		foreach ( $boolean_queries as $boolean_query ) {
1687 14
			if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
1688 1
				$query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
1689
			}
1690
		}
1691
1692
		// These queries cannot be auto-generated so we have to remove them and build them manually.
1693
		$manual_queries = array(
1694 14
			'sku'        => '',
1695
			'featured'   => '',
1696
			'visibility' => '',
1697
		);
1698 14
		foreach ( $manual_queries as $key => $manual_query ) {
1699 14
			if ( isset( $query_vars[ $key ] ) ) {
1700 14
				$manual_queries[ $key ] = $query_vars[ $key ];
1701 14
				unset( $query_vars[ $key ] );
1702
			}
1703
		}
1704
1705 14
		$wp_query_args = parent::get_wp_query_args( $query_vars );
1706
1707 14
		if ( ! isset( $wp_query_args['date_query'] ) ) {
1708 14
			$wp_query_args['date_query'] = array();
1709
		}
1710 14
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
1711
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
0 ignored issues
show
introduced by
Detected usage of meta_query, possible slow query.
Loading history...
1712
		}
1713
1714
		// Handle product types.
1715 14
		if ( 'variation' === $query_vars['type'] ) {
1716 1
			$wp_query_args['post_type'] = 'product_variation';
1717 14
		} elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
1718 1
			$wp_query_args['post_type']   = array( 'product_variation', 'product' );
1719 1
			$wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1720 1
				'relation' => 'OR',
1721
				array(
1722 1
					'taxonomy' => 'product_type',
1723 1
					'field'    => 'slug',
1724 1
					'terms'    => $query_vars['type'],
1725
				),
1726
				array(
1727
					'taxonomy' => 'product_type',
1728
					'field'    => 'id',
1729
					'operator' => 'NOT EXISTS',
1730
				),
1731
			);
1732 View Code Duplication
		} else {
1733 13
			$wp_query_args['post_type']   = 'product';
1734 13
			$wp_query_args['tax_query'][] = array(
1735 13
				'taxonomy' => 'product_type',
1736 13
				'field'    => 'slug',
1737 13
				'terms'    => $query_vars['type'],
1738
			);
1739
		}
1740
1741
		// Handle product categories.
1742 14 View Code Duplication
		if ( ! empty( $query_vars['category'] ) ) {
1743 1
			$wp_query_args['tax_query'][] = array(
1744 1
				'taxonomy' => 'product_cat',
1745 1
				'field'    => 'slug',
1746 1
				'terms'    => $query_vars['category'],
1747
			);
1748
		}
1749
1750
		// Handle product tags.
1751 14 View Code Duplication
		if ( ! empty( $query_vars['tag'] ) ) {
1752 1
			unset( $wp_query_args['tag'] );
1753 1
			$wp_query_args['tax_query'][] = array(
1754 1
				'taxonomy' => 'product_tag',
1755 1
				'field'    => 'slug',
1756 1
				'terms'    => $query_vars['tag'],
1757
			);
1758
		}
1759
1760
		// Handle shipping classes.
1761 14 View Code Duplication
		if ( ! empty( $query_vars['shipping_class'] ) ) {
1762 1
			$wp_query_args['tax_query'][] = array(
1763 1
				'taxonomy' => 'product_shipping_class',
1764 1
				'field'    => 'slug',
1765 1
				'terms'    => $query_vars['shipping_class'],
1766
			);
1767
		}
1768
1769
		// Handle total_sales.
1770
		// This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
1771 14
		if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
1772 1
			$wp_query_args['meta_query'][] = array(
1773 1
				'key'     => 'total_sales',
1774 1
				'value'   => absint( $query_vars['total_sales'] ),
1775 1
				'compare' => '=',
1776
			);
1777
		}
1778
1779
		// Handle SKU.
1780 14
		if ( $manual_queries['sku'] ) {
1781
			// Check for existing values if wildcard is used.
1782 2
			if ( '*' === $manual_queries['sku'] ) {
1783 1
				$wp_query_args['meta_query'][] = array(
1784
					array(
1785 1
						'key'     => '_sku',
1786
						'compare' => 'EXISTS',
1787
					),
1788
					array(
1789
						'key'     => '_sku',
1790
						'value'   => '',
1791
						'compare' => '!=',
1792
					),
1793
				);
1794
			} else {
1795 2
				$wp_query_args['meta_query'][] = array(
1796 2
					'key'     => '_sku',
1797 2
					'value'   => $manual_queries['sku'],
1798 2
					'compare' => 'LIKE',
1799
				);
1800
			}
1801
		}
1802
1803
		// Handle featured.
1804 14
		if ( '' !== $manual_queries['featured'] ) {
1805 1
			$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1806 1
			if ( $manual_queries['featured'] ) {
1807 1
				$wp_query_args['tax_query'][] = array(
1808 1
					'taxonomy' => 'product_visibility',
1809 1
					'field'    => 'term_taxonomy_id',
1810 1
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1811
				);
1812 1
				$wp_query_args['tax_query'][] = array(
1813 1
					'taxonomy' => 'product_visibility',
1814 1
					'field'    => 'term_taxonomy_id',
1815 1
					'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
1816 1
					'operator' => 'NOT IN',
1817
				);
1818
			} else {
1819 1
				$wp_query_args['tax_query'][] = array(
1820 1
					'taxonomy' => 'product_visibility',
1821 1
					'field'    => 'term_taxonomy_id',
1822 1
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1823 1
					'operator' => 'NOT IN',
1824
				);
1825
			}
1826
		}
1827
1828
		// Handle visibility.
1829 14
		if ( $manual_queries['visibility'] ) {
1830 1
			switch ( $manual_queries['visibility'] ) {
1831 View Code Duplication
				case 'search':
1832 1
					$wp_query_args['tax_query'][] = array(
1833
						'taxonomy' => 'product_visibility',
1834
						'field'    => 'slug',
1835
						'terms'    => array( 'exclude-from-search' ),
1836
						'operator' => 'NOT IN',
1837
					);
1838 1
					break;
1839 View Code Duplication
				case 'catalog':
1840
					$wp_query_args['tax_query'][] = array(
1841
						'taxonomy' => 'product_visibility',
1842
						'field'    => 'slug',
1843
						'terms'    => array( 'exclude-from-catalog' ),
1844
						'operator' => 'NOT IN',
1845
					);
1846
					break;
1847 View Code Duplication
				case 'visible':
1848 1
					$wp_query_args['tax_query'][] = array(
1849
						'taxonomy' => 'product_visibility',
1850
						'field'    => 'slug',
1851
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1852
						'operator' => 'NOT IN',
1853
					);
1854 1
					break;
1855 View Code Duplication
				case 'hidden':
1856 1
					$wp_query_args['tax_query'][] = array(
1857
						'taxonomy' => 'product_visibility',
1858
						'field'    => 'slug',
1859
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1860
						'operator' => 'AND',
1861
					);
1862 1
					break;
1863
			}
1864
		}
1865
1866
		// Handle date queries.
1867
		$date_queries = array(
1868 14
			'date_created'      => 'post_date',
1869
			'date_modified'     => 'post_modified',
1870
			'date_on_sale_from' => '_sale_price_dates_from',
1871
			'date_on_sale_to'   => '_sale_price_dates_to',
1872
		);
1873 14
		foreach ( $date_queries as $query_var_key => $db_key ) {
1874 14
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {
1875
1876
				// Remove any existing meta queries for the same keys to prevent conflicts.
1877 1
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
1878 1
				foreach ( $existing_queries as $query_index => $query_contents ) {
1879
					unset( $wp_query_args['meta_query'][ $query_index ] );
1880
				}
1881
1882 1
				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
1883
			}
1884
		}
1885
1886
		// Handle paginate.
1887 14 View Code Duplication
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
1888 13
			$wp_query_args['no_found_rows'] = true;
1889
		}
1890
1891
		// Handle reviews_allowed.
1892 14
		if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
1893 1
			add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
1894
		}
1895
1896 14
		return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
1897
	}
1898
1899
	/**
1900
	 * Query for Products matching specific criteria.
1901
	 *
1902
	 * @since 3.2.0
1903
	 *
1904
	 * @param array $query_vars Query vars from a WC_Product_Query.
1905
	 *
1906
	 * @return array|object
1907
	 */
1908 14
	public function query( $query_vars ) {
1909 14
		$args = $this->get_wp_query_args( $query_vars );
1910
1911 14 View Code Duplication
		if ( ! empty( $args['errors'] ) ) {
1912
			$query = (object) array(
1913
				'posts'         => array(),
1914
				'found_posts'   => 0,
1915
				'max_num_pages' => 0,
1916
			);
1917
		} else {
1918 14
			$query = new WP_Query( $args );
1919
		}
1920
1921 14
		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
1922
			// Prime caches before grabbing objects.
1923 4
			update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
1924
		}
1925
1926 14
		$products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );
1927
1928 14 View Code Duplication
		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
1929
			return (object) array(
1930 2
				'products'      => $products,
1931 2
				'total'         => $query->found_posts,
1932 2
				'max_num_pages' => $query->max_num_pages,
1933
			);
1934
		}
1935
1936 13
		return $products;
1937
	}
1938
1939
	/**
1940
	 * Get data to save to a lookup table.
1941
	 *
1942
	 * @since 3.6.0
1943
	 * @param int    $id ID of object to update.
1944
	 * @param string $table Lookup table name.
1945
	 * @return array
1946
	 */
1947 399
	protected function get_data_for_lookup_table( $id, $table ) {
1948 399
		if ( 'wc_product_meta_lookup' === $table ) {
1949 399
			$price_meta   = (array) get_post_meta( $id, '_price', false );
1950 399
			$manage_stock = get_post_meta( $id, '_manage_stock', true );
1951 399
			$stock        = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
1952 399
			$price        = wc_format_decimal( get_post_meta( $id, '_price', true ) );
1953 399
			$sale_price   = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
1954
			return array(
1955 399
				'product_id'     => absint( $id ),
1956 399
				'sku'            => get_post_meta( $id, '_sku', true ),
1957 399
				'virtual'        => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
1958 399
				'downloadable'   => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
1959 399
				'min_price'      => reset( $price_meta ),
1960 399
				'max_price'      => end( $price_meta ),
1961 399
				'onsale'         => $sale_price && $price === $sale_price ? 1 : 0,
1962 399
				'stock_quantity' => $stock,
1963 399
				'stock_status'   => get_post_meta( $id, '_stock_status', true ),
1964 399
				'rating_count'   => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ),
1965 399
				'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
1966 399
				'total_sales'    => get_post_meta( $id, 'total_sales', true ),
1967
			);
1968
		}
1969
		return array();
1970
	}
1971
1972
	/**
1973
	 * Get primary key name for lookup table.
1974
	 *
1975
	 * @since 3.6.0
1976
	 * @param string $table Lookup table name.
1977
	 * @return string
1978
	 */
1979 10
	protected function get_primary_key_for_lookup_table( $table ) {
1980 10
		if ( 'wc_product_meta_lookup' === $table ) {
1981 10
			return 'product_id';
1982
		}
1983
		return '';
1984
	}
1985
}
1986