Completed
Push — master ( 870ea9...fbe455 )
by Claudio
52:27 queued 44:39
created

WC_Product_Data_Store_CPT::read_stock_quantity()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 1
nop 2
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 2
rs 10
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 12
				'post_content'   => $product->get_description( 'edit' ),
203 12
				'post_excerpt'   => $product->get_short_description( 'edit' ),
204 12
				'post_title'     => $product->get_name( 'edit' ),
205 12
				'post_parent'    => $product->get_parent_id( 'edit' ),
206 12
				'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
207 12
				'post_status'    => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
208 12
				'menu_order'     => $product->get_menu_order( 'edit' ),
209 12
				'post_password'  => $product->get_post_password( 'edit' ),
210 12
				'post_name'      => $product->get_slug( 'edit' ),
211 12
				'post_type'      => 'product',
212
			);
213 12
			if ( $product->get_date_created( 'edit' ) ) {
214 12
				$post_data['post_date']     = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
215 12
				$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
216
			}
217 12
			if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
218
				$post_data['post_modified']     = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
219
				$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 12 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 12
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
238
			}
239 12
			$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
	 * Re-reads stock from the DB ignoring changes.
368
	 *
369
	 * @param WC_Product $product Product object.
370
	 * @param int|float  $new_stock New stock level if already read.
371
	 */
372 2
	public function read_stock_quantity( &$product, $new_stock = null ) {
373 2
		$object_read = $product->get_object_read();
374 2
		$product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db).
375 2
		$product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock );
376 2
		$product->set_object_read( $object_read );
377
	}
378
379
	/**
380
	 * Read extra data associated with the product, like button text or product URL for external products.
381
	 *
382
	 * @param WC_Product $product Product object.
383
	 * @since 3.0.0
384
	 */
385 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...
386 374
		foreach ( $product->get_extra_data_keys() as $key ) {
387 22
			$function = 'set_' . $key;
388 22
			if ( is_callable( array( $product, $function ) ) ) {
389 22
				$product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
390
			}
391
		}
392
	}
393
394
	/**
395
	 * Convert visibility terms to props.
396
	 * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
397
	 *
398
	 * @param WC_Product $product Product object.
399
	 * @since 3.0.0
400
	 */
401 368
	protected function read_visibility( &$product ) {
402 368
		$terms           = get_the_terms( $product->get_id(), 'product_visibility' );
403 368
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
404 368
		$featured        = in_array( 'featured', $term_names, true );
405 368
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
406 368
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
407
408 368 View Code Duplication
		if ( $exclude_search && $exclude_catalog ) {
409 1
			$catalog_visibility = 'hidden';
410 368
		} elseif ( $exclude_search ) {
411 1
			$catalog_visibility = 'catalog';
412 368
		} elseif ( $exclude_catalog ) {
413 2
			$catalog_visibility = 'search';
414
		} else {
415 367
			$catalog_visibility = 'visible';
416
		}
417
418 368
		$product->set_props(
419
			array(
420 368
				'featured'           => $featured,
421 368
				'catalog_visibility' => $catalog_visibility,
422
			)
423
		);
424
	}
425
426
	/**
427
	 * Read attributes from post meta.
428
	 *
429
	 * @param WC_Product $product Product object.
430
	 */
431 327
	protected function read_attributes( &$product ) {
432 327
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
433
434 327
		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
435 3
			$attributes = array();
436 3
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
437 3
				$meta_value = array_merge(
438
					array(
439 3
						'name'         => '',
440
						'value'        => '',
441
						'position'     => 0,
442
						'is_visible'   => 0,
443
						'is_variation' => 0,
444
						'is_taxonomy'  => 0,
445
					),
446 3
					(array) $meta_attribute_value
447
				);
448
449
				// Check if is a taxonomy attribute.
450 3 View Code Duplication
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
451
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
452
						continue;
453
					}
454
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
455
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
456
				} else {
457 3
					$id      = 0;
458 3
					$options = wc_get_text_attributes( $meta_value['value'] );
459
				}
460
461 3
				$attribute = new WC_Product_Attribute();
462 3
				$attribute->set_id( $id );
463 3
				$attribute->set_name( $meta_value['name'] );
464 3
				$attribute->set_options( $options );
465 3
				$attribute->set_position( $meta_value['position'] );
466 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...
467 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...
468 3
				$attributes[] = $attribute;
469
			}
470 3
			$product->set_attributes( $attributes );
471
		}
472
	}
473
474
	/**
475
	 * Read downloads from post meta.
476
	 *
477
	 * @param WC_Product $product Product object.
478
	 * @since 3.0.0
479
	 */
480 374
	protected function read_downloads( &$product ) {
481 374
		$meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );
482
483 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...
484 3
			$downloads = array();
485 3
			foreach ( $meta_values as $key => $value ) {
486 3
				if ( ! isset( $value['name'], $value['file'] ) ) {
487
					continue;
488
				}
489 3
				$download = new WC_Product_Download();
490 3
				$download->set_id( $key );
491 3
				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
492 3
				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
493 3
				$downloads[] = $download;
494
			}
495 3
			$product->set_downloads( $downloads );
496
		}
497
	}
498
499
	/**
500
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
501
	 *
502
	 * @param WC_Product $product Product object.
503
	 * @param bool       $force Force update. Used during create.
504
	 * @since 3.0.0
505
	 */
506 399
	protected function update_post_meta( &$product, $force = false ) {
507
		$meta_key_to_props = array(
508 399
			'_sku'                   => 'sku',
509
			'_regular_price'         => 'regular_price',
510
			'_sale_price'            => 'sale_price',
511
			'_sale_price_dates_from' => 'date_on_sale_from',
512
			'_sale_price_dates_to'   => 'date_on_sale_to',
513
			'total_sales'            => 'total_sales',
514
			'_tax_status'            => 'tax_status',
515
			'_tax_class'             => 'tax_class',
516
			'_manage_stock'          => 'manage_stock',
517
			'_backorders'            => 'backorders',
518
			'_low_stock_amount'      => 'low_stock_amount',
519
			'_sold_individually'     => 'sold_individually',
520
			'_weight'                => 'weight',
521
			'_length'                => 'length',
522
			'_width'                 => 'width',
523
			'_height'                => 'height',
524
			'_upsell_ids'            => 'upsell_ids',
525
			'_crosssell_ids'         => 'cross_sell_ids',
526
			'_purchase_note'         => 'purchase_note',
527
			'_default_attributes'    => 'default_attributes',
528
			'_virtual'               => 'virtual',
529
			'_downloadable'          => 'downloadable',
530
			'_product_image_gallery' => 'gallery_image_ids',
531
			'_download_limit'        => 'download_limit',
532
			'_download_expiry'       => 'download_expiry',
533
			'_thumbnail_id'          => 'image_id',
534
			'_stock'                 => 'stock_quantity',
535
			'_stock_status'          => 'stock_status',
536
			'_wc_average_rating'     => 'average_rating',
537
			'_wc_rating_count'       => 'rating_counts',
538
			'_wc_review_count'       => 'review_count',
539
		);
540
541
		// Make sure to take extra data (like product url or text for external products) into account.
542 399
		$extra_data_keys = $product->get_extra_data_keys();
543
544 399
		foreach ( $extra_data_keys as $key ) {
545 23
			$meta_key_to_props[ '_' . $key ] = $key;
546
		}
547
548 399
		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
549
550 399
		foreach ( $props_to_update as $meta_key => $prop ) {
551 399
			$value = $product->{"get_$prop"}( 'edit' );
552 399
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
553 399
			switch ( $prop ) {
554
				case 'virtual':
555
				case 'downloadable':
556
				case 'manage_stock':
557
				case 'sold_individually':
558 399
					$value = wc_bool_to_string( $value );
559 399
					break;
560
				case 'gallery_image_ids':
561 399
					$value = implode( ',', $value );
562 399
					break;
563
				case 'date_on_sale_from':
564
				case 'date_on_sale_to':
565 399
					$value = $value ? $value->getTimestamp() : '';
566 399
					break;
567
			}
568
569 399
			$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
570
571 399
			if ( $updated ) {
572 399
				$this->updated_props[] = $prop;
573
			}
574
		}
575
576
		// Update extra data associated with the product like button text or product URL for external products.
577 399
		if ( ! $this->extra_data_saved ) {
578 399
			foreach ( $extra_data_keys as $key ) {
579 23
				$meta_key = '_' . $key;
580 23
				$function = 'get_' . $key;
581 23
				if ( ! array_key_exists( $meta_key, $props_to_update ) ) {
582 1
					continue;
583
				}
584 23
				if ( is_callable( array( $product, $function ) ) ) {
585 23
					$value   = $product->{$function}( 'edit' );
586 23
					$value   = is_string( $value ) ? wp_slash( $value ) : $value;
587 23
					$updated = $this->update_or_delete_post_meta( $product, $meta_key, $value );
588
589 23
					if ( $updated ) {
590
						$this->updated_props[] = $key;
591
					}
592
				}
593
			}
594
		}
595
596 399
		if ( $this->update_downloads( $product, $force ) ) {
597 3
			$this->updated_props[] = 'downloads';
598
		}
599
	}
600
601
	/**
602
	 * Handle updated meta props after updating meta data.
603
	 *
604
	 * @since 3.0.0
605
	 * @param WC_Product $product Product Object.
606
	 */
607 399
	protected function handle_updated_props( &$product ) {
608 399
		$price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );
609
610 399
		if ( ! $price_is_synced ) {
611 396
			if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
612 355
				if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
613
					update_post_meta( $product->get_id(), '_sale_price', '' );
614
					$product->set_sale_price( '' );
615
				}
616
			}
617
618 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 ) ) {
619 356
				if ( $product->is_on_sale( 'edit' ) ) {
620 16
					update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
621 16
					$product->set_price( $product->get_sale_price( 'edit' ) );
622
				} else {
623 355
					update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
624 355
					$product->set_price( $product->get_regular_price( 'edit' ) );
625
				}
626
			}
627
		}
628
629 399
		if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
630 399
			if ( $product->is_type( 'variation' ) ) {
631 63
				do_action( 'woocommerce_variation_set_stock', $product );
632
			} else {
633 398
				do_action( 'woocommerce_product_set_stock', $product );
634
			}
635
		}
636
637 399
		if ( in_array( 'stock_status', $this->updated_props, true ) ) {
638 399
			if ( $product->is_type( 'variation' ) ) {
639 63
				do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
640
			} else {
641 398
				do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
642
			}
643
		}
644
645 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' ) ) ) {
646 399
			$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
647
		}
648
649
		// Trigger action so 3rd parties can deal with updated props.
650 399
		do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );
651
652
		// After handling, we can reset the props array.
653 399
		$this->updated_props = array();
654
	}
655
656
	/**
657
	 * For all stored terms in all taxonomies, save them to the DB.
658
	 *
659
	 * @param WC_Product $product Product object.
660
	 * @param bool       $force Force update. Used during create.
661
	 * @since 3.0.0
662
	 */
663 398
	protected function update_terms( &$product, $force = false ) {
664 398
		$changes = $product->get_changes();
665
666 398
		if ( $force || array_key_exists( 'category_ids', $changes ) ) {
667 398
			$categories = $product->get_category_ids( 'edit' );
668
669 398
			if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
670 389
				$categories = array( get_option( 'default_product_cat', 0 ) );
671
			}
672
673 398
			wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
674
		}
675 398
		if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
676 398
			wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
677
		}
678 398 View Code Duplication
		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
679 398
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
680
		}
681
	}
682
683
	/**
684
	 * Update visibility terms based on props.
685
	 *
686
	 * @since 3.0.0
687
	 *
688
	 * @param WC_Product $product Product object.
689
	 * @param bool       $force Force update. Used during create.
690
	 */
691 398
	protected function update_visibility( &$product, $force = false ) {
692 398
		$changes = $product->get_changes();
693
694 398
		if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
695 398
			$terms = array();
696
697 398
			if ( $product->get_featured() ) {
698 4
				$terms[] = 'featured';
699
			}
700
701 398
			if ( 'outofstock' === $product->get_stock_status() ) {
702 68
				$terms[] = 'outofstock';
703
			}
704
705 398
			$rating = min( 5, round( $product->get_average_rating(), 0 ) );
706
707 398
			if ( $rating > 0 ) {
708 1
				$terms[] = 'rated-' . $rating;
709
			}
710
711 398
			switch ( $product->get_catalog_visibility() ) {
712
				case 'hidden':
713 2
					$terms[] = 'exclude-from-search';
714 2
					$terms[] = 'exclude-from-catalog';
715 2
					break;
716
				case 'catalog':
717 1
					$terms[] = 'exclude-from-search';
718 1
					break;
719
				case 'search':
720 3
					$terms[] = 'exclude-from-catalog';
721 3
					break;
722
			}
723
724 398
			if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
725 398
				do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
726
			}
727
		}
728
	}
729
730
	/**
731
	 * Update attributes which are a mix of terms and meta data.
732
	 *
733
	 * @param WC_Product $product Product object.
734
	 * @param bool       $force Force update. Used during create.
735
	 * @since 3.0.0
736
	 */
737 398
	protected function update_attributes( &$product, $force = false ) {
738 398
		$changes = $product->get_changes();
739
740 398
		if ( $force || array_key_exists( 'attributes', $changes ) ) {
741 398
			$attributes  = $product->get_attributes();
742 398
			$meta_values = array();
743
744 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...
745 56
				foreach ( $attributes as $attribute_key => $attribute ) {
746 56
					$value = '';
747
748 56
					if ( is_null( $attribute ) ) {
749
						if ( taxonomy_exists( $attribute_key ) ) {
750
							// Handle attributes that have been unset.
751
							wp_set_object_terms( $product->get_id(), array(), $attribute_key );
752
						} elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) {
753
							// Handle attributes that have been unset.
754
							wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) );
755
						}
756
						continue;
757
758 56
					} elseif ( $attribute->is_taxonomy() ) {
759 46
						wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
760
					} else {
761 13
						$value = wc_implode_text_attributes( $attribute->get_options() );
762
					}
763
764
					// Store in format WC uses in meta.
765 56
					$meta_values[ $attribute_key ] = array(
766 56
						'name'         => $attribute->get_name(),
767 56
						'value'        => $value,
768 56
						'position'     => $attribute->get_position(),
769 56
						'is_visible'   => $attribute->get_visible() ? 1 : 0,
770 56
						'is_variation' => $attribute->get_variation() ? 1 : 0,
771 56
						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
772
					);
773
				}
774
			}
775
			// Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround.
776 398
			$this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) );
777
		}
778
	}
779
780
	/**
781
	 * Update downloads.
782
	 *
783
	 * @since 3.0.0
784
	 * @param WC_Product $product Product object.
785
	 * @param bool       $force Force update. Used during create.
786
	 * @return bool If updated or not.
787
	 */
788 399
	protected function update_downloads( &$product, $force = false ) {
789 399
		$changes = $product->get_changes();
790
791 399
		if ( $force || array_key_exists( 'downloads', $changes ) ) {
792 399
			$downloads   = $product->get_downloads();
793 399
			$meta_values = array();
794
795 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...
796 3
				foreach ( $downloads as $key => $download ) {
797
					// Store in format WC uses in meta.
798 3
					$meta_values[ $key ] = $download->get_data();
799
				}
800
			}
801
802 399
			if ( $product->is_type( 'variation' ) ) {
803 63
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
804
			} else {
805 398
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
806
			}
807
808 399
			return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) );
809
		}
810 119
		return false;
811
	}
812
813
	/**
814
	 * Make sure we store the product type and version (to track data changes).
815
	 *
816
	 * @param WC_Product $product Product object.
817
	 * @since 3.0.0
818
	 */
819 398
	protected function update_version_and_type( &$product ) {
820 398
		$old_type = WC_Product_Factory::get_product_type( $product->get_id() );
821 398
		$new_type = $product->get_type();
822
823 398
		wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
824 398
		update_post_meta( $product->get_id(), '_product_version', WC_VERSION );
825
826
		// Action for the transition.
827 398
		if ( $old_type !== $new_type ) {
828 76
			$this->updated_props[] = 'product_type';
829 76
			do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
830
		}
831
	}
832
833
	/**
834
	 * Clear any caches.
835
	 *
836
	 * @param WC_Product $product Product object.
837
	 * @since 3.0.0
838
	 */
839 399
	protected function clear_caches( &$product ) {
840 399
		wc_delete_product_transients( $product->get_id() );
841 399
		if ( $product->get_parent_id( 'edit' ) ) {
842 61
			wc_delete_product_transients( $product->get_parent_id( 'edit' ) );
843 61
			WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_parent_id( 'edit' ) );
844
		}
845 399
		WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) );
846 399
		WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_id() );
847
	}
848
849
	/*
850
	|--------------------------------------------------------------------------
851
	| wc-product-functions.php methods
852
	|--------------------------------------------------------------------------
853
	*/
854
855
	/**
856
	 * Returns an array of on sale products, as an array of objects with an
857
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
858
	 *
859
	 * @return array
860
	 * @since 3.0.0
861
	 */
862 4
	public function get_on_sale_products() {
863
		global $wpdb;
864
865 4
		$exclude_term_ids            = array();
866 4
		$outofstock_join             = '';
867 4
		$outofstock_where            = '';
868 4
		$non_published_where         = '';
0 ignored issues
show
Unused Code introduced by
$non_published_where is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
869 4
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
870
871 4 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
875 4
		if ( count( $exclude_term_ids ) ) {
876
			$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
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
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
			AND posts.post_parent NOT IN (
892 4
				SELECT ID FROM `$wpdb->posts` as posts
893
				WHERE posts.post_type = 'product'
894
				AND posts.post_parent = 0
895
				AND posts.post_status != 'publish'
896
			)
897
			GROUP BY posts.ID
898
			"
899
			// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
900
		);
901
	}
902
903
	/**
904
	 * Returns a list of product IDs ( id as key => parent as value) that are
905
	 * featured. Uses get_posts instead of wc_get_products since we want
906
	 * some extra meta queries and ALL products (posts_per_page = -1).
907
	 *
908
	 * @return array
909
	 * @since 3.0.0
910
	 */
911 2
	public function get_featured_product_ids() {
912 2
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
913
914 2
		return get_posts(
915
			array(
916 2
				'post_type'      => array( 'product', 'product_variation' ),
917
				'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...
918 2
				'post_status'    => 'publish',
919
				'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...
920 2
					'relation' => 'AND',
921
					array(
922 2
						'taxonomy' => 'product_visibility',
923 2
						'field'    => 'term_taxonomy_id',
924 2
						'terms'    => array( $product_visibility_term_ids['featured'] ),
925
					),
926
					array(
927 2
						'taxonomy' => 'product_visibility',
928 2
						'field'    => 'term_taxonomy_id',
929 2
						'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
930 2
						'operator' => 'NOT IN',
931
					),
932
				),
933 2
				'fields'         => 'id=>parent',
934
			)
935
		);
936
	}
937
938
	/**
939
	 * Check if product sku is found for any other product IDs.
940
	 *
941
	 * @since 3.0.0
942
	 * @param int    $product_id Product ID.
943
	 * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
944
	 * @return bool
945
	 */
946 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...
947
		global $wpdb;
948
949
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
950 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...
951 341
			$wpdb->prepare(
952 341
				"
953
				SELECT posts.ID
954 341
				FROM {$wpdb->posts} as posts
955 341
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
956
				WHERE
957
				posts.post_type IN ( 'product', 'product_variation' )
958
				AND posts.post_status != 'trash'
959
				AND lookup.sku = %s
960
				AND lookup.product_id <> %d
961
				LIMIT 1
962
				",
963 341
				wp_slash( $sku ),
964
				$product_id
965
			)
966
		);
967
	}
968
969
	/**
970
	 * Return product ID based on SKU.
971
	 *
972
	 * @since 3.0.0
973
	 * @param string $sku Product SKU.
974
	 * @return int
975
	 */
976 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...
977
		global $wpdb;
978
979
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
980 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...
981 94
			$wpdb->prepare(
982 94
				"
983
				SELECT posts.ID
984 94
				FROM {$wpdb->posts} as posts
985 94
				INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
986
				WHERE
987
				posts.post_type IN ( 'product', 'product_variation' )
988
				AND posts.post_status != 'trash'
989
				AND lookup.sku = %s
990
				LIMIT 1
991
				",
992
				$sku
993
			)
994
		);
995
996 94
		return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
997
	}
998
999
	/**
1000
	 * Returns an array of IDs of products that have sales starting soon.
1001
	 *
1002
	 * @since 3.0.0
1003
	 * @return array
1004
	 */
1005
	public function get_starting_sales() {
1006
		global $wpdb;
1007
1008
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1009
		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...
1010
			$wpdb->prepare(
1011
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1012
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1013
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1014
				WHERE postmeta.meta_key = '_sale_price_dates_from'
1015
					AND postmeta_2.meta_key = '_price'
1016
					AND postmeta_3.meta_key = '_sale_price'
1017
					AND postmeta.meta_value > 0
1018
					AND postmeta.meta_value < %s
1019
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1020
				current_time( 'timestamp', true )
1021
			)
1022
		);
1023
	}
1024
1025
	/**
1026
	 * Returns an array of IDs of products that have sales which are due to end.
1027
	 *
1028
	 * @since 3.0.0
1029
	 * @return array
1030
	 */
1031
	public function get_ending_sales() {
1032
		global $wpdb;
1033
1034
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1035
		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...
1036
			$wpdb->prepare(
1037
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1038
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1039
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1040
				WHERE postmeta.meta_key = '_sale_price_dates_to'
1041
					AND postmeta_2.meta_key = '_price'
1042
					AND postmeta_3.meta_key = '_regular_price'
1043
					AND postmeta.meta_value > 0
1044
					AND postmeta.meta_value < %s
1045
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1046
				current_time( 'timestamp', true )
1047
			)
1048
		);
1049
	}
1050
1051
	/**
1052
	 * Find a matching (enabled) variation within a variable product.
1053
	 *
1054
	 * @since  3.0.0
1055
	 * @param  WC_Product $product Variable product.
1056
	 * @param  array      $match_attributes Array of attributes we want to try to match.
1057
	 * @return int Matching variation ID or 0.
1058
	 */
1059
	public function find_matching_product_variation( $product, $match_attributes = array() ) {
1060
		global $wpdb;
1061
1062
		$meta_attribute_names = array();
1063
1064
		// Get attributes to match in meta.
1065
		foreach ( $product->get_attributes() as $attribute ) {
1066
			if ( ! $attribute->get_variation() ) {
1067
				continue;
1068
			}
1069
1070
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() );
1071
1072
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
1073
				return 0;
1074
			}
1075
1076
			$meta_attribute_names[] = $attribute_field_name;
1077
		}
1078
1079
		// Get the attributes of the variations.
1080
		$query = $wpdb->prepare(
1081
			"
1082
			SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta}
1083
			WHERE post_id IN (
1084
				SELECT ID FROM {$wpdb->posts}
1085
				WHERE {$wpdb->posts}.post_parent = %d
1086
				AND {$wpdb->posts}.post_status = 'publish'
1087
				AND {$wpdb->posts}.post_type = 'product_variation'
1088
				ORDER BY menu_order ASC, ID ASC
1089
			)
1090
			",
1091
			$product->get_id()
1092
		);
1093
1094
		$query .= ' AND meta_key IN ( "' . implode( '","', array_map( 'esc_sql', $meta_attribute_names ) ) . '" );';
1095
1096
		$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...
1097
1098
		if ( ! $attributes ) {
1099
			return 0;
1100
		}
1101
1102
		$sorted_meta = array();
1103
1104
		foreach ( $attributes as $m ) {
1105
			$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...
1106
		}
1107
1108
		/**
1109
		 * Check each variation to find the one that matches the $match_attributes.
1110
		 *
1111
		 * Note: Not all meta fields will be set which is why we check existance.
1112
		 */
1113
		foreach ( $sorted_meta as $variation_id => $variation ) {
1114
			$match = true;
1115
1116
			foreach ( $match_attributes as $attribute_key => $attribute_value ) {
1117
				if ( array_key_exists( $attribute_key, $variation ) ) {
1118
					if ( $variation[ $attribute_key ] !== $attribute_value && ! empty( $variation[ $attribute_key ] ) ) {
1119
						$match = false;
1120
					}
1121
				}
1122
			}
1123
1124
			if ( true === $match ) {
1125
				return $variation_id;
1126
			}
1127
		}
1128
1129
		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
1130
			/**
1131
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
1132
			 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
1133
			 */
1134
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
1135
		}
1136
	}
1137
1138
	/**
1139
	 * Creates all possible combinations of variations from the attributes, without creating duplicates.
1140
	 *
1141
	 * @since  3.6.0
1142
	 * @todo   Add to interface in 4.0.
1143
	 * @param  WC_Product $product Variable product.
1144
	 * @param  int        $limit Limit the number of created variations.
1145
	 * @return int        Number of created variations.
1146
	 */
1147 2
	public function create_all_product_variations( $product, $limit = -1 ) {
1148 2
		$count = 0;
1149
1150 2
		if ( ! $product ) {
1151
			return $count;
1152
		}
1153
1154 2
		$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );
1155
1156 2
		if ( empty( $attributes ) ) {
1157
			return $count;
1158
		}
1159
1160
		// Get existing variations so we don't create duplicates.
1161 2
		$existing_variations = array_map( 'wc_get_product', $product->get_children() );
1162 2
		$existing_attributes = array();
1163
1164 2
		foreach ( $existing_variations as $existing_variation ) {
1165 2
			$existing_attributes[] = $existing_variation->get_attributes();
1166
		}
1167
1168 2
		$possible_attributes = array_reverse( wc_array_cartesian( $attributes ) );
1169
1170 2
		foreach ( $possible_attributes as $possible_attribute ) {
1171
			// Allow any order if key/values -- do not use strict mode.
1172 2
			if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1173 2
				continue;
1174
			}
1175 2
			$variation = new WC_Product_Variation();
1176 2
			$variation->set_parent_id( $product->get_id() );
1177 2
			$variation->set_attributes( $possible_attribute );
1178 2
			$variation_id = $variation->save();
1179
1180 2
			do_action( 'product_variation_linked', $variation_id );
1181
1182 2
			$count ++;
1183
1184 2
			if ( $limit > 0 && $count >= $limit ) {
1185 1
				break;
1186
			}
1187
		}
1188
1189 2
		return $count;
1190
	}
1191
1192
	/**
1193
	 * Make sure all variations have a sort order set so they can be reordered correctly.
1194
	 *
1195
	 * @param int $parent_id Product ID.
1196
	 */
1197
	public function sort_all_product_variations( $parent_id ) {
1198
		global $wpdb;
1199
1200
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1201
		$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...
1202
			$wpdb->prepare(
1203
				"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",
1204
				$parent_id
1205
			)
1206
		);
1207
		$index = 1;
1208
1209
		foreach ( $ids as $id ) {
1210
			// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1211
			$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...
1212
		}
1213
	}
1214
1215
	/**
1216
	 * Return a list of related products (using data like categories and IDs).
1217
	 *
1218
	 * @since 3.0.0
1219
	 * @param array $cats_array  List of categories IDs.
1220
	 * @param array $tags_array  List of tags IDs.
1221
	 * @param array $exclude_ids Excluded IDs.
1222
	 * @param int   $limit       Limit of results.
1223
	 * @param int   $product_id  Product ID.
1224
	 * @return array
1225
	 */
1226 27
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
1227
		global $wpdb;
1228
1229
		$args = array(
1230 27
			'categories'  => $cats_array,
1231 27
			'tags'        => $tags_array,
1232 27
			'exclude_ids' => $exclude_ids,
1233 27
			'limit'       => $limit + 10,
1234
		);
1235
1236 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 );
1237
1238
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
1239 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...
1240
	}
1241
1242
	/**
1243
	 * Builds the related posts query.
1244
	 *
1245
	 * @since 3.0.0
1246
	 *
1247
	 * @param array $cats_array  List of categories IDs.
1248
	 * @param array $tags_array  List of tags IDs.
1249
	 * @param array $exclude_ids Excluded IDs.
1250
	 * @param int   $limit       Limit of results.
1251
	 *
1252
	 * @return array
1253
	 */
1254 27
	public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
1255
		global $wpdb;
1256
1257 27
		$include_term_ids            = array_merge( $cats_array, $tags_array );
1258 27
		$exclude_term_ids            = array();
1259 27
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1260
1261 27
		if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
1262 27
			$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
1263
		}
1264
1265 27 View Code Duplication
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
1266
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
1267
		}
1268
1269
		$query = array(
1270
			'fields' => "
1271 27
				SELECT DISTINCT ID FROM {$wpdb->posts} p
1272
			",
1273 27
			'join'   => '',
1274 27
			'where'  => "
1275
				WHERE 1=1
1276
				AND p.post_status = 'publish'
1277
				AND p.post_type = 'product'
1278
1279
			",
1280
			'limits' => '
0 ignored issues
show
introduced by
Each line in an array declaration must end in a comma
Loading history...
1281 27
				LIMIT ' . absint( $limit ) . '
1282
			',
1283
		);
1284
1285 27 View Code Duplication
		if ( count( $exclude_term_ids ) ) {
1286 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';
1287 27
			$query['where'] .= ' AND exclude_join.object_id IS NULL';
1288
		}
1289
1290 27 View Code Duplication
		if ( count( $include_term_ids ) ) {
1291 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';
1292
		}
1293
1294 27
		if ( count( $exclude_ids ) ) {
1295 27
			$query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
1296
		}
1297
1298 27
		return $query;
1299
	}
1300
1301
	/**
1302
	 * Update a product's stock amount directly in the database.
1303
	 *
1304
	 * Updates both post meta and lookup tables. Ignores manage stock setting on the product.
1305
	 *
1306
	 * @param int            $product_id_with_stock Product ID.
1307
	 * @param int|float|null $stock_quantity        Stock quantity.
1308
	 */
1309
	protected function set_product_stock( $product_id_with_stock, $stock_quantity ) {
1310
		global $wpdb;
1311
1312
		// Generate SQL.
1313
		$sql = $wpdb->prepare(
1314
			"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
1315
			$stock_quantity,
1316
			$product_id_with_stock
1317
		);
1318
1319
		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' );
1320
1321
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, 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...
1322
1323
		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
1324
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
1325
		wp_cache_delete( $product_id_with_stock, 'post_meta' );
1326
1327
		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );
1328
	}
1329
1330
	/**
1331
	 * Update a product's stock amount directly.
1332
	 *
1333
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
1334
	 * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables.
1335
	 * Uses locking to update the quantity. If the lock is not acquired, change is lost.
1336
	 *
1337
	 * @since  3.0.0 this supports set, increase and decrease.
1338
	 * @param  int            $product_id_with_stock Product ID.
1339
	 * @param  int|float|null $stock_quantity Stock quantity.
1340
	 * @param  string         $operation Set, increase and decrease.
1341
	 * @return int|float New stock level.
1342
	 */
1343 2
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
1344
		global $wpdb;
1345
1346
		// Ensures a row exists to update.
1347 2
		add_post_meta( $product_id_with_stock, '_stock', 0, true );
1348
1349 2
		if ( 'set' === $operation ) {
1350 2
			$new_stock = wc_stock_amount( $stock_quantity );
1351
		} else {
1352
			// @todo: potential race condition.
1353
			// Read current stock level and lock the row. If the lock can't be acquired, don't wait.
1354 1
			$current_stock = wc_stock_amount(
1355 1
				$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...
1356 1
					$wpdb->prepare(
1357 1
						"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';",
1358
						$product_id_with_stock
1359
					)
1360
				)
1361
			);
1362
1363
			// Calculate new value.
1364 1
			switch ( $operation ) {
1365
				case 'increase':
1366 1
					$new_stock = $current_stock + wc_stock_amount( $stock_quantity );
1367 1
					break;
1368
				default:
1369 1
					$new_stock = $current_stock - wc_stock_amount( $stock_quantity );
1370 1
					break;
1371
			}
1372
		}
1373
1374
		// Generate SQL.
1375 2
		$sql = $wpdb->prepare(
1376 2
			"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
1377
			$new_stock,
1378
			$product_id_with_stock
1379
		);
1380
1381 2
		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' );
1382
1383 2
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, 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...
1384
1385
		// Cache delete is required (not only) to set correct data for lookup table (which reads from cache).
1386
		// Sometimes I wonder if it shouldn't be part of update_lookup_table.
1387 2
		wp_cache_delete( $product_id_with_stock, 'post_meta' );
1388
1389 2
		$this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' );
1390
1391
		/**
1392
		 * Fire an action for this direct update so it can be detected by other code.
1393
		 *
1394
		 * @since 3.6
1395
		 * @param int $product_id_with_stock Product ID that was updated directly.
1396
		 */
1397 2
		do_action( 'woocommerce_updated_product_stock', $product_id_with_stock );
1398
1399 2
		return $new_stock;
1400
	}
1401
1402
	/**
1403
	 * Update a product's sale count directly.
1404
	 *
1405
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
1406
	 *
1407
	 * @since  3.0.0 this supports set, increase and decrease.
1408
	 * @param  int      $product_id Product ID.
1409
	 * @param  int|null $quantity Quantity.
1410
	 * @param  string   $operation set, increase and decrease.
1411
	 */
1412 13
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
1413
		global $wpdb;
1414 13
		add_post_meta( $product_id, 'total_sales', 0, true );
1415
1416
		// Update stock in DB directly.
1417 13
		switch ( $operation ) {
1418
			case 'increase':
1419
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1420 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...
1421 13
					$wpdb->prepare(
1422 13
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'",
1423
						$quantity,
1424
						$product_id
1425
					)
1426
				);
1427 13
				break;
1428
			case 'decrease':
1429
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1430
				$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...
1431
					$wpdb->prepare(
1432
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'",
1433
						$quantity,
1434
						$product_id
1435
					)
1436
				);
1437
				break;
1438
			default:
1439
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1440
				$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...
1441
					$wpdb->prepare(
1442
						"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'",
1443
						$quantity,
1444
						$product_id
1445
					)
1446
				);
1447
				break;
1448
		}
1449
1450 13
		wp_cache_delete( $product_id, 'post_meta' );
1451
1452 13
		$this->update_lookup_table( $product_id, 'wc_product_meta_lookup' );
1453
1454
		/**
1455
		 * Fire an action for this direct update so it can be detected by other code.
1456
		 *
1457
		 * @since 3.6
1458
		 * @param int $product_id Product ID that was updated directly.
1459
		 */
1460 13
		do_action( 'woocommerce_updated_product_sales', $product_id );
1461
	}
1462
1463
	/**
1464
	 * Update a products average rating meta.
1465
	 *
1466
	 * @since 3.0.0
1467
	 * @todo Deprecate unused function?
1468
	 * @param WC_Product $product Product object.
1469
	 */
1470
	public function update_average_rating( $product ) {
1471
		update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
1472
		self::update_visibility( $product, true );
1473
	}
1474
1475
	/**
1476
	 * Update a products review count meta.
1477
	 *
1478
	 * @since 3.0.0
1479
	 * @todo Deprecate unused function?
1480
	 * @param WC_Product $product Product object.
1481
	 */
1482
	public function update_review_count( $product ) {
1483
		update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
1484
	}
1485
1486
	/**
1487
	 * Update a products rating counts.
1488
	 *
1489
	 * @since 3.0.0
1490
	 * @todo Deprecate unused function?
1491
	 * @param WC_Product $product Product object.
1492
	 */
1493
	public function update_rating_counts( $product ) {
1494
		update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
1495
	}
1496
1497
	/**
1498
	 * Get shipping class ID by slug.
1499
	 *
1500
	 * @since 3.0.0
1501
	 * @param string $slug Product shipping class slug.
1502
	 * @return int|false
1503
	 */
1504 2
	public function get_shipping_class_id_by_slug( $slug ) {
1505 2
		$shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
1506 2
		if ( $shipping_class_term ) {
1507 2
			return $shipping_class_term->term_id;
1508
		} else {
1509
			return false;
1510
		}
1511
	}
1512
1513
	/**
1514
	 * Returns an array of products.
1515
	 *
1516
	 * @param  array $args Args to pass to WC_Product_Query().
1517
	 * @return array|object
1518
	 * @see wc_get_products
1519
	 */
1520
	public function get_products( $args = array() ) {
1521
		$query = new WC_Product_Query( $args );
1522
		return $query->get_products();
1523
	}
1524
1525
	/**
1526
	 * Search product data for a term and return ids.
1527
	 *
1528
	 * @param  string     $term Search term.
1529
	 * @param  string     $type Type of product.
1530
	 * @param  bool       $include_variations Include variations in search or not.
1531
	 * @param  bool       $all_statuses Should we search all statuses or limit to published.
1532
	 * @param  null|int   $limit Limit returned results. @since 3.5.0.
1533
	 * @param  null|array $include Keep specific results. @since 3.6.0.
1534
	 * @param  null|array $exclude Discard specific results. @since 3.6.0.
1535
	 * @return array of ids
1536
	 */
1537 1
	public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) {
1538
		global $wpdb;
1539
1540 1
		$custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit );
1541
1542 1
		if ( is_array( $custom_results ) ) {
1543
			return $custom_results;
1544
		}
1545
1546 1
		$post_types    = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
1547 1
		$post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' );
1548 1
		$type_where    = '';
1549 1
		$status_where  = '';
1550 1
		$limit_query   = '';
1551 1
		$term          = wc_strtolower( $term );
1552
1553
		// See if search term contains OR keywords.
1554 1
		if ( strstr( $term, ' or ' ) ) {
1555 1
			$term_groups = explode( ' or ', $term );
1556
		} else {
1557 1
			$term_groups = array( $term );
1558
		}
1559
1560 1
		$search_where   = '';
1561 1
		$search_queries = array();
1562
1563 1
		foreach ( $term_groups as $term_group ) {
1564
			// Parse search terms.
1565 1
			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
1566 1
				$search_terms = $this->get_valid_search_terms( $matches[0] );
1567 1
				$count        = count( $search_terms );
1568
1569
				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
1570 1
				if ( 9 < $count || 0 === $count ) {
1571 1
					$search_terms = array( $term_group );
1572
				}
1573
			} else {
1574
				$search_terms = array( $term_group );
1575
			}
1576
1577 1
			$term_group_query = '';
1578 1
			$searchand        = '';
1579
1580 1
			foreach ( $search_terms as $search_term ) {
1581 1
				$like              = '%' . $wpdb->esc_like( $search_term ) . '%';
1582 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.
1583 1
				$searchand         = ' AND ';
1584
			}
1585
1586 1
			if ( $term_group_query ) {
1587 1
				$search_queries[] = $term_group_query;
1588
			}
1589
		}
1590
1591 1
		if ( ! empty( $search_queries ) ) {
1592 1
			$search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') ';
1593
		}
1594
1595 1 View Code Duplication
		if ( ! empty( $include ) && is_array( $include ) ) {
1596 1
			$search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') ';
1597
		}
1598
1599 1 View Code Duplication
		if ( ! empty( $exclude ) && is_array( $exclude ) ) {
1600 1
			$search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') ';
1601
		}
1602
1603 1
		if ( 'virtual' === $type ) {
1604
			$type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) ';
1605 1
		} elseif ( 'downloadable' === $type ) {
1606
			$type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) ';
1607
		}
1608
1609 1
		if ( ! $all_statuses ) {
1610
			$status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
1611
		}
1612
1613 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...
1614 1
			$limit_query = $wpdb->prepare( ' LIMIT %d ', $limit );
1615
		}
1616
1617
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1618 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...
1619
			// phpcs:disable
1620 1
			"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
1621 1
			 LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
1622 1
			WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
1623 1
			$search_where
1624 1
			$status_where
1625 1
			$type_where
1626
			ORDER BY posts.post_parent ASC, posts.post_title ASC
1627 1
			$limit_query
1628
			"
1629
			// phpcs:enable
1630
		);
1631
1632 1
		$product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );
1633
1634 1
		if ( is_numeric( $term ) ) {
1635
			$post_id   = absint( $term );
1636
			$post_type = get_post_type( $post_id );
1637
1638
			if ( 'product_variation' === $post_type && $include_variations ) {
1639
				$product_ids[] = $post_id;
1640
			} elseif ( 'product' === $post_type ) {
1641
				$product_ids[] = $post_id;
1642
			}
1643
1644
			$product_ids[] = wp_get_post_parent_id( $post_id );
1645
		}
1646
1647 1
		return wp_parse_id_list( $product_ids );
1648
	}
1649
1650
	/**
1651
	 * Get the product type based on product ID.
1652
	 *
1653
	 * @since 3.0.0
1654
	 * @param int $product_id Product ID.
1655
	 * @return bool|string
1656
	 */
1657 401
	public function get_product_type( $product_id ) {
1658 401
		$cache_key    = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
1659 401
		$product_type = wp_cache_get( $cache_key, 'products' );
1660
1661 401
		if ( $product_type ) {
1662 231
			return $product_type;
1663
		}
1664
1665 401
		$post_type = get_post_type( $product_id );
1666
1667 401
		if ( 'product_variation' === $post_type ) {
1668 63
			$product_type = 'variation';
1669 400
		} elseif ( 'product' === $post_type ) {
1670 398
			$terms        = get_the_terms( $product_id, 'product_type' );
1671 398
			$product_type = ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
1672
		} else {
1673 2
			$product_type = false;
1674
		}
1675
1676 401
		wp_cache_set( $cache_key, $product_type, 'products' );
1677
1678 401
		return $product_type;
1679
	}
1680
1681
	/**
1682
	 * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
1683
	 *
1684
	 * @since 3.2.0
1685
	 * @param string   $where Where clause.
1686
	 * @param WP_Query $wp_query WP_Query instance.
1687
	 * @return string
1688
	 */
1689 1
	public function reviews_allowed_query_where( $where, $wp_query ) {
1690
		global $wpdb;
1691
1692 1
		if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
1693 1
			if ( $wp_query->query_vars['reviews_allowed'] ) {
1694 1
				$where .= " AND $wpdb->posts.comment_status = 'open'";
1695
			} else {
1696 1
				$where .= " AND $wpdb->posts.comment_status = 'closed'";
1697
			}
1698
		}
1699
1700 1
		return $where;
1701
	}
1702
1703
	/**
1704
	 * Get valid WP_Query args from a WC_Product_Query's query variables.
1705
	 *
1706
	 * @since 3.2.0
1707
	 * @param array $query_vars Query vars from a WC_Product_Query.
1708
	 * @return array
1709
	 */
1710 14
	protected function get_wp_query_args( $query_vars ) {
1711
1712
		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
1713
		$key_mapping = array(
1714 14
			'status'         => 'post_status',
1715
			'page'           => 'paged',
1716
			'include'        => 'post__in',
1717
			'stock_quantity' => 'stock',
1718
			'average_rating' => 'wc_average_rating',
1719
			'review_count'   => 'wc_review_count',
1720
		);
1721 14 View Code Duplication
		foreach ( $key_mapping as $query_key => $db_key ) {
1722 14
			if ( isset( $query_vars[ $query_key ] ) ) {
1723 14
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
1724 14
				unset( $query_vars[ $query_key ] );
1725
			}
1726
		}
1727
1728
		// Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
1729
		$boolean_queries = array(
1730 14
			'virtual',
1731
			'downloadable',
1732
			'sold_individually',
1733
			'manage_stock',
1734
		);
1735 14
		foreach ( $boolean_queries as $boolean_query ) {
1736 14
			if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
1737 1
				$query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
1738
			}
1739
		}
1740
1741
		// These queries cannot be auto-generated so we have to remove them and build them manually.
1742
		$manual_queries = array(
1743 14
			'sku'        => '',
1744
			'featured'   => '',
1745
			'visibility' => '',
1746
		);
1747 14
		foreach ( $manual_queries as $key => $manual_query ) {
1748 14
			if ( isset( $query_vars[ $key ] ) ) {
1749 14
				$manual_queries[ $key ] = $query_vars[ $key ];
1750 14
				unset( $query_vars[ $key ] );
1751
			}
1752
		}
1753
1754 14
		$wp_query_args = parent::get_wp_query_args( $query_vars );
1755
1756 14
		if ( ! isset( $wp_query_args['date_query'] ) ) {
1757 14
			$wp_query_args['date_query'] = array();
1758
		}
1759 14
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
1760
			$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...
1761
		}
1762
1763
		// Handle product types.
1764 14
		if ( 'variation' === $query_vars['type'] ) {
1765 1
			$wp_query_args['post_type'] = 'product_variation';
1766 14
		} elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
1767 1
			$wp_query_args['post_type']   = array( 'product_variation', 'product' );
1768 1
			$wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1769 1
				'relation' => 'OR',
1770
				array(
1771 1
					'taxonomy' => 'product_type',
1772 1
					'field'    => 'slug',
1773 1
					'terms'    => $query_vars['type'],
1774
				),
1775
				array(
1776
					'taxonomy' => 'product_type',
1777
					'field'    => 'id',
1778
					'operator' => 'NOT EXISTS',
1779
				),
1780
			);
1781 View Code Duplication
		} else {
1782 13
			$wp_query_args['post_type']   = 'product';
1783 13
			$wp_query_args['tax_query'][] = array(
1784 13
				'taxonomy' => 'product_type',
1785 13
				'field'    => 'slug',
1786 13
				'terms'    => $query_vars['type'],
1787
			);
1788
		}
1789
1790
		// Handle product categories.
1791 14 View Code Duplication
		if ( ! empty( $query_vars['category'] ) ) {
1792 1
			$wp_query_args['tax_query'][] = array(
1793 1
				'taxonomy' => 'product_cat',
1794 1
				'field'    => 'slug',
1795 1
				'terms'    => $query_vars['category'],
1796
			);
1797
		}
1798
1799
		// Handle product tags.
1800 14 View Code Duplication
		if ( ! empty( $query_vars['tag'] ) ) {
1801 1
			unset( $wp_query_args['tag'] );
1802 1
			$wp_query_args['tax_query'][] = array(
1803 1
				'taxonomy' => 'product_tag',
1804 1
				'field'    => 'slug',
1805 1
				'terms'    => $query_vars['tag'],
1806
			);
1807
		}
1808
1809
		// Handle shipping classes.
1810 14 View Code Duplication
		if ( ! empty( $query_vars['shipping_class'] ) ) {
1811 1
			$wp_query_args['tax_query'][] = array(
1812 1
				'taxonomy' => 'product_shipping_class',
1813 1
				'field'    => 'slug',
1814 1
				'terms'    => $query_vars['shipping_class'],
1815
			);
1816
		}
1817
1818
		// Handle total_sales.
1819
		// This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
1820 14
		if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
1821 1
			$wp_query_args['meta_query'][] = array(
1822 1
				'key'     => 'total_sales',
1823 1
				'value'   => absint( $query_vars['total_sales'] ),
1824 1
				'compare' => '=',
1825
			);
1826
		}
1827
1828
		// Handle SKU.
1829 14
		if ( $manual_queries['sku'] ) {
1830
			// Check for existing values if wildcard is used.
1831 2
			if ( '*' === $manual_queries['sku'] ) {
1832 1
				$wp_query_args['meta_query'][] = array(
1833
					array(
1834 1
						'key'     => '_sku',
1835
						'compare' => 'EXISTS',
1836
					),
1837
					array(
1838
						'key'     => '_sku',
1839
						'value'   => '',
1840
						'compare' => '!=',
1841
					),
1842
				);
1843
			} else {
1844 2
				$wp_query_args['meta_query'][] = array(
1845 2
					'key'     => '_sku',
1846 2
					'value'   => $manual_queries['sku'],
1847 2
					'compare' => 'LIKE',
1848
				);
1849
			}
1850
		}
1851
1852
		// Handle featured.
1853 14
		if ( '' !== $manual_queries['featured'] ) {
1854 1
			$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1855 1
			if ( $manual_queries['featured'] ) {
1856 1
				$wp_query_args['tax_query'][] = array(
1857 1
					'taxonomy' => 'product_visibility',
1858 1
					'field'    => 'term_taxonomy_id',
1859 1
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1860
				);
1861 1
				$wp_query_args['tax_query'][] = array(
1862 1
					'taxonomy' => 'product_visibility',
1863 1
					'field'    => 'term_taxonomy_id',
1864 1
					'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
1865 1
					'operator' => 'NOT IN',
1866
				);
1867
			} else {
1868 1
				$wp_query_args['tax_query'][] = array(
1869 1
					'taxonomy' => 'product_visibility',
1870 1
					'field'    => 'term_taxonomy_id',
1871 1
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1872 1
					'operator' => 'NOT IN',
1873
				);
1874
			}
1875
		}
1876
1877
		// Handle visibility.
1878 14
		if ( $manual_queries['visibility'] ) {
1879 1
			switch ( $manual_queries['visibility'] ) {
1880 View Code Duplication
				case 'search':
1881 1
					$wp_query_args['tax_query'][] = array(
1882
						'taxonomy' => 'product_visibility',
1883
						'field'    => 'slug',
1884
						'terms'    => array( 'exclude-from-search' ),
1885
						'operator' => 'NOT IN',
1886
					);
1887 1
					break;
1888 View Code Duplication
				case 'catalog':
1889
					$wp_query_args['tax_query'][] = array(
1890
						'taxonomy' => 'product_visibility',
1891
						'field'    => 'slug',
1892
						'terms'    => array( 'exclude-from-catalog' ),
1893
						'operator' => 'NOT IN',
1894
					);
1895
					break;
1896 View Code Duplication
				case 'visible':
1897 1
					$wp_query_args['tax_query'][] = array(
1898
						'taxonomy' => 'product_visibility',
1899
						'field'    => 'slug',
1900
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1901
						'operator' => 'NOT IN',
1902
					);
1903 1
					break;
1904 View Code Duplication
				case 'hidden':
1905 1
					$wp_query_args['tax_query'][] = array(
1906
						'taxonomy' => 'product_visibility',
1907
						'field'    => 'slug',
1908
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1909
						'operator' => 'AND',
1910
					);
1911 1
					break;
1912
			}
1913
		}
1914
1915
		// Handle date queries.
1916
		$date_queries = array(
1917 14
			'date_created'      => 'post_date',
1918
			'date_modified'     => 'post_modified',
1919
			'date_on_sale_from' => '_sale_price_dates_from',
1920
			'date_on_sale_to'   => '_sale_price_dates_to',
1921
		);
1922 14
		foreach ( $date_queries as $query_var_key => $db_key ) {
1923 14
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {
1924
1925
				// Remove any existing meta queries for the same keys to prevent conflicts.
1926 1
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
1927 1
				foreach ( $existing_queries as $query_index => $query_contents ) {
1928
					unset( $wp_query_args['meta_query'][ $query_index ] );
1929
				}
1930
1931 1
				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
1932
			}
1933
		}
1934
1935
		// Handle paginate.
1936 14 View Code Duplication
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
1937 13
			$wp_query_args['no_found_rows'] = true;
1938
		}
1939
1940
		// Handle reviews_allowed.
1941 14
		if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
1942 1
			add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
1943
		}
1944
1945 14
		return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
1946
	}
1947
1948
	/**
1949
	 * Query for Products matching specific criteria.
1950
	 *
1951
	 * @since 3.2.0
1952
	 *
1953
	 * @param array $query_vars Query vars from a WC_Product_Query.
1954
	 *
1955
	 * @return array|object
1956
	 */
1957 14
	public function query( $query_vars ) {
1958 14
		$args = $this->get_wp_query_args( $query_vars );
1959
1960 14 View Code Duplication
		if ( ! empty( $args['errors'] ) ) {
1961
			$query = (object) array(
1962
				'posts'         => array(),
1963
				'found_posts'   => 0,
1964
				'max_num_pages' => 0,
1965
			);
1966
		} else {
1967 14
			$query = new WP_Query( $args );
1968
		}
1969
1970 14
		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
1971
			// Prime caches before grabbing objects.
1972 4
			update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
1973
		}
1974
1975 14
		$products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );
1976
1977 14 View Code Duplication
		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
1978
			return (object) array(
1979 2
				'products'      => $products,
1980 2
				'total'         => $query->found_posts,
1981 2
				'max_num_pages' => $query->max_num_pages,
1982
			);
1983
		}
1984
1985 13
		return $products;
1986
	}
1987
1988
	/**
1989
	 * Get data to save to a lookup table.
1990
	 *
1991
	 * @since 3.6.0
1992
	 * @param int    $id ID of object to update.
1993
	 * @param string $table Lookup table name.
1994
	 * @return array
1995
	 */
1996 399
	protected function get_data_for_lookup_table( $id, $table ) {
1997 399
		if ( 'wc_product_meta_lookup' === $table ) {
1998 399
			$price_meta   = (array) get_post_meta( $id, '_price', false );
1999 399
			$manage_stock = get_post_meta( $id, '_manage_stock', true );
2000 399
			$stock        = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
2001 399
			$price        = wc_format_decimal( get_post_meta( $id, '_price', true ) );
2002 399
			$sale_price   = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
2003
			return array(
2004 399
				'product_id'     => absint( $id ),
2005 399
				'sku'            => get_post_meta( $id, '_sku', true ),
2006 399
				'virtual'        => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
2007 399
				'downloadable'   => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0,
2008 399
				'min_price'      => reset( $price_meta ),
2009 399
				'max_price'      => end( $price_meta ),
2010 399
				'onsale'         => $sale_price && $price === $sale_price ? 1 : 0,
2011 399
				'stock_quantity' => $stock,
2012 399
				'stock_status'   => get_post_meta( $id, '_stock_status', true ),
2013 399
				'rating_count'   => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ),
2014 399
				'average_rating' => get_post_meta( $id, '_wc_average_rating', true ),
2015 399
				'total_sales'    => get_post_meta( $id, 'total_sales', true ),
2016
			);
2017
		}
2018
		return array();
2019
	}
2020
2021
	/**
2022
	 * Get primary key name for lookup table.
2023
	 *
2024
	 * @since 3.6.0
2025
	 * @param string $table Lookup table name.
2026
	 * @return string
2027
	 */
2028 10
	protected function get_primary_key_for_lookup_table( $table ) {
2029 10
		if ( 'wc_product_meta_lookup' === $table ) {
2030 10
			return 'product_id';
2031
		}
2032
		return '';
2033
	}
2034
}
2035