Completed
Push — master ( fc6097...930cc3 )
by Mike
08:01
created

WC_Product::get_availability_class()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 8
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 10
rs 8.8571
1
<?php
2
/**
3
 * Abstract Product Class
4
 *
5
 * The WooCommerce product class handles individual product data.
6
 *
7
 * @class       WC_Product
8
 * @var         WP_Post
9
 * @version     2.1.0
10
 * @package     WooCommerce/Abstracts
11
 * @category    Abstract Class
12
 * @author      WooThemes
13
 *
14
 * @property    string $width Product width
15
 * @property    string $length Product length
16
 * @property    string $height Product height
17
 * @property    string $weight Product weight
18
 * @property    string $price Product price
19
 * @property    string $regular_price Product regular price
20
 * @property    string $sale_price Product sale price
21
 * @property    string $product_image_gallery String of image IDs in the gallery
22
 * @property    string $sku Product SKU
23
 * @property    string $stock Stock amount
24
 * @property    string $downloadable Shows/define if the product is downloadable
25
 * @property    string $virtual Shows/define if the product is virtual
26
 * @property    string $sold_individually Allow one item to be bought in a single order
27
 * @property    string $tax_status Tax status
28
 * @property    string $tax_class Tax class
29
 * @property    string $manage_stock Shows/define if can manage the product stock
30
 * @property    string $stock_status Stock status
31
 * @property    string $backorders Whether or not backorders are allowed
32
 * @property    string $featured Featured product
33
 * @property    string $visibility Product visibility
34
 * @property    string $variation_id Variation ID when dealing with variations
35
 */
36
class WC_Product {
37
38
	/**
39
	 * The product (post) ID.
40
	 *
41
	 * @var int
42
	 */
43
	public $id = 0;
44
45
	/**
46
	 * $post Stores post data.
47
	 *
48
	 * @var $post WP_Post
49
	 */
50
	public $post = null;
51
52
	/**
53
	 * The product's type (simple, variable etc).
54
	 *
55
	 * @var string
56
	 */
57
	public $product_type = null;
58
59
	/**
60
	 * Product shipping class.
61
	 *
62
	 * @var string
63
	 */
64
	protected $shipping_class    = '';
65
66
	/**
67
	 * ID of the shipping class this product has.
68
	 *
69
	 * @var int
70
	 */
71
	protected $shipping_class_id = 0;
72
73
	/** @public string The product's total stock, including that of its children. */
74
	public $total_stock;
75
76
	/**
77
	 * Supported features such as 'ajax_add_to_cart'.
78
	 * @var array
79
	 */
80
	protected $supports = array();
81
82
	/**
83
	 * Constructor gets the post object and sets the ID for the loaded product.
84
	 *
85
	 * @param int|WC_Product|object $product Product ID, post object, or product object
86
	 */
87
	public function __construct( $product ) {
88
		if ( is_numeric( $product ) ) {
89
			$this->id   = absint( $product );
90
			$this->post = get_post( $this->id );
91
		} elseif ( $product instanceof WC_Product ) {
92
			$this->id   = absint( $product->id );
93
			$this->post = $product->post;
94
		} elseif ( isset( $product->ID ) ) {
95
			$this->id   = absint( $product->ID );
96
			$this->post = $product;
97
		}
98
	}
99
100
	/**
101
	 * __isset function.
102
	 *
103
	 * @param mixed $key
104
	 * @return bool
105
	 */
106
	public function __isset( $key ) {
107
		return metadata_exists( 'post', $this->id, '_' . $key );
108
	}
109
110
	/**
111
	 * __get function.
112
	 *
113
	 * @param string $key
114
	 * @return mixed
115
	 */
116
	public function __get( $key ) {
117
		$value = get_post_meta( $this->id, '_' . $key, true );
118
119
		// Get values or default if not set
120
		if ( in_array( $key, array( 'downloadable', 'virtual', 'backorders', 'manage_stock', 'featured', 'sold_individually' ) ) ) {
121
			$value = $value ? $value : 'no';
122
123
		} elseif ( in_array( $key, array( 'product_attributes', 'crosssell_ids', 'upsell_ids' ) ) ) {
124
			$value = $value ? $value : array();
125
126
		} elseif ( 'visibility' === $key ) {
127
			$value = $value ? $value : 'hidden';
128
129
		} elseif ( 'stock' === $key ) {
130
			$value = $value ? $value : 0;
131
132
		} elseif ( 'stock_status' === $key ) {
133
			$value = $value ? $value : 'instock';
134
135
		} elseif ( 'tax_status' === $key ) {
136
			$value = $value ? $value : 'taxable';
137
138
		}
139
140
		if ( false !== $value ) {
141
			$this->$key = $value;
142
		}
143
144
		return $value;
145
	}
146
147
	/**
148
	 * Get the product's post data.
149
	 *
150
	 * @return object
151
	 */
152
	public function get_post_data() {
153
		return $this->post;
154
	}
155
156
	/**
157
	 * Check if a product supports a given feature.
158
	 *
159
	 * Product classes should override this to declare support (or lack of support) for a feature.
160
	 *
161
	 * @param string $feature string The name of a feature to test support for.
162
	 * @return bool True if the product supports the feature, false otherwise.
163
	 * @since 2.5.0
164
	 */
165
	public function supports( $feature ) {
166
		return apply_filters( 'woocommerce_product_supports', in_array( $feature, $this->supports ) ? true : false, $feature, $this );
167
	}
168
169
	/**
170
	 * Return the product ID
171
	 *
172
	 * @since 2.5.0
173
	 * @return int product (post) ID
174
	 */
175
	public function get_id() {
176
177
		return $this->id;
178
	}
179
180
	/**
181
	 * Returns the gallery attachment ids.
182
	 *
183
	 * @return array
184
	 */
185
	public function get_gallery_attachment_ids() {
186
		return apply_filters( 'woocommerce_product_gallery_attachment_ids', array_filter( array_filter( (array) explode( ',', $this->product_image_gallery ) ), 'wp_attachment_is_image' ), $this );
187
	}
188
189
	/**
190
	 * Wrapper for get_permalink.
191
	 *
192
	 * @return string
193
	 */
194
	public function get_permalink() {
195
		return get_permalink( $this->id );
196
	}
197
198
	/**
199
	 * Get SKU (Stock-keeping unit) - product unique ID.
200
	 *
201
	 * @return string
202
	 */
203
	public function get_sku() {
204
		return apply_filters( 'woocommerce_get_sku', $this->sku, $this );
205
	}
206
207
	/**
208
	 * Returns number of items available for sale.
209
	 *
210
	 * @return int
211
	 */
212
	public function get_stock_quantity() {
213
		return apply_filters( 'woocommerce_get_stock_quantity', $this->managing_stock() ? wc_stock_amount( $this->stock ) : null, $this );
214
	}
215
216
	/**
217
	 * Get total stock - This is the stock of parent and children combined.
218
	 *
219
	 * @return int
220
	 */
221
	public function get_total_stock() {
222
		if ( empty( $this->total_stock ) ) {
223
			if ( sizeof( $this->get_children() ) > 0 ) {
224
				$this->total_stock = max( 0, $this->get_stock_quantity() );
225
226
				foreach ( $this->get_children() as $child_id ) {
227
					if ( 'yes' === get_post_meta( $child_id, '_manage_stock', true ) ) {
228
						$stock = get_post_meta( $child_id, '_stock', true );
229
						$this->total_stock += max( 0, wc_stock_amount( $stock ) );
230
					}
231
				}
232
			} else {
233
				$this->total_stock = $this->get_stock_quantity();
234
			}
235
		}
236
		return wc_stock_amount( $this->total_stock );
237
	}
238
239
	/**
240
	 * Check if the stock status needs changing.
241
	 */
242
	public function check_stock_status() {
243
		if ( ! $this->backorders_allowed() && $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
244
			if ( $this->stock_status !== 'outofstock' ) {
245
				$this->set_stock_status( 'outofstock' );
246
			}
247
		} elseif ( $this->backorders_allowed() || $this->get_total_stock() > get_option( 'woocommerce_notify_no_stock_amount' ) ) {
248
			if ( $this->stock_status !== 'instock' ) {
249
				$this->set_stock_status( 'instock' );
250
			}
251
		}
252
	}
253
254
	/**
255
	 * Set stock level of the product.
256
	 *
257
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
258
	 * We cannot rely on the original loaded value in case another order was made since then.
259
	 *
260
	 * @param int $amount (default: null)
261
	 * @param string $mode can be set, add, or subtract
262
	 * @return int new stock level
263
	 */
264
	public function set_stock( $amount = null, $mode = 'set' ) {
265
		global $wpdb;
266
267
		if ( ! is_null( $amount ) && $this->managing_stock() ) {
268
269
			// Ensure key exists
270
			add_post_meta( $this->id, '_stock', 0, true );
271
272
			// Update stock in DB directly
273 View Code Duplication
			switch ( $mode ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
274
				case 'add' :
275
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
276
				break;
277
				case 'subtract' :
278
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
279
				break;
280
				default :
281
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
282
				break;
283
			}
284
285
			// Clear caches
286
			wp_cache_delete( $this->id, 'post_meta' );
287
			delete_transient( 'wc_low_stock_count' );
288
			delete_transient( 'wc_outofstock_count' );
289
			unset( $this->stock );
290
291
			// Stock status
292
			$this->check_stock_status();
293
294
			// Trigger action
295
			do_action( 'woocommerce_product_set_stock', $this );
296
		}
297
298
		return $this->get_stock_quantity();
299
	}
300
301
	/**
302
	 * Reduce stock level of the product.
303
	 *
304
	 * @param int $amount Amount to reduce by. Default: 1
305
	 * @return int new stock level
306
	 */
307
	public function reduce_stock( $amount = 1 ) {
308
		return $this->set_stock( $amount, 'subtract' );
309
	}
310
311
	/**
312
	 * Increase stock level of the product.
313
	 *
314
	 * @param int $amount Amount to increase by. Default 1.
315
	 * @return int new stock level
316
	 */
317
	public function increase_stock( $amount = 1 ) {
318
		return $this->set_stock( $amount, 'add' );
319
	}
320
321
	/**
322
	 * Set stock status of the product.
323
	 *
324
	 * @param string $status
325
	 */
326
	public function set_stock_status( $status ) {
327
328
		$status = ( 'outofstock' === $status ) ? 'outofstock' : 'instock';
329
330
		// Sanity check
331 View Code Duplication
		if ( $this->managing_stock() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
332
			if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
333
				$status = 'outofstock';
334
			}
335
		}
336
337
		if ( update_post_meta( $this->id, '_stock_status', $status ) ) {
338
			$this->stock_status = $status;
339
			do_action( 'woocommerce_product_set_stock_status', $this->id, $status );
340
		}
341
	}
342
343
	/**
344
	 * Return the product type.
345
	 *
346
	 * @return string
347
	 */
348
	public function get_type() {
349
		return is_null( $this->product_type ) ? '' : $this->product_type;
350
	}
351
352
	/**
353
	 * Checks the product type.
354
	 *
355
	 * Backwards compat with downloadable/virtual.
356
	 *
357
	 * @param string $type Array or string of types
358
	 * @return bool
359
	 */
360
	public function is_type( $type ) {
361
		return ( $this->product_type == $type || ( is_array( $type ) && in_array( $this->product_type, $type ) ) ) ? true : false;
362
	}
363
364
	/**
365
	 * Checks if a product is downloadable.
366
	 *
367
	 * @return bool
368
	 */
369
	public function is_downloadable() {
370
		return $this->downloadable == 'yes' ? true : false;
371
	}
372
373
	/**
374
	 * Check if downloadable product has a file attached.
375
	 *
376
	 * @since 1.6.2
377
	 *
378
	 * @param string $download_id file identifier
379
	 * @return bool Whether downloadable product has a file attached.
380
	 */
381
	public function has_file( $download_id = '' ) {
382
		return ( $this->is_downloadable() && $this->get_file( $download_id ) ) ? true : false;
383
	}
384
385
	/**
386
	 * Gets an array of downloadable files for this product.
387
	 *
388
	 * @since 2.1.0
389
	 *
390
	 * @return array
391
	 */
392
	public function get_files() {
393
394
		$downloadable_files = array_filter( isset( $this->downloadable_files ) ? (array) maybe_unserialize( $this->downloadable_files ) : array() );
395
396
		if ( ! empty( $downloadable_files ) ) {
397
398
			foreach ( $downloadable_files as $key => $file ) {
399
400
				if ( ! is_array( $file ) ) {
401
					$downloadable_files[ $key ] = array(
402
						'file' => $file,
403
						'name' => ''
404
					);
405
				}
406
407
				// Set default name
408
				if ( empty( $file['name'] ) ) {
409
					$downloadable_files[ $key ]['name'] = wc_get_filename_from_url( $file['file'] );
410
				}
411
412
				// Filter URL
413
				$downloadable_files[ $key ]['file'] = apply_filters( 'woocommerce_file_download_path', $downloadable_files[ $key ]['file'], $this, $key );
414
			}
415
		}
416
417
		return apply_filters( 'woocommerce_product_files', $downloadable_files, $this );
418
	}
419
420
	/**
421
	 * Get a file by $download_id.
422
	 *
423
	 * @param string $download_id file identifier
424
	 * @return array|false if not found
425
	 */
426
	public function get_file( $download_id = '' ) {
427
428
		$files = $this->get_files();
429
430
		if ( '' === $download_id ) {
431
			$file = sizeof( $files ) ? current( $files ) : false;
432
		} elseif ( isset( $files[ $download_id ] ) ) {
433
			$file = $files[ $download_id ];
434
		} else {
435
			$file = false;
436
		}
437
438
		// allow overriding based on the particular file being requested
439
		return apply_filters( 'woocommerce_product_file', $file, $this, $download_id );
440
	}
441
442
	/**
443
	 * Get file download path identified by $download_id.
444
	 *
445
	 * @param string $download_id file identifier
446
	 * @return string
447
	 */
448
	public function get_file_download_path( $download_id ) {
449
		$files = $this->get_files();
450
451
		if ( isset( $files[ $download_id ] ) ) {
452
			$file_path = $files[ $download_id ]['file'];
453
		} else {
454
			$file_path = '';
455
		}
456
457
		// allow overriding based on the particular file being requested
458
		return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id );
459
	}
460
461
	/**
462
	 * Checks if a product is virtual (has no shipping).
463
	 *
464
	 * @return bool
465
	 */
466
	public function is_virtual() {
467
		return apply_filters( 'woocommerce_is_virtual', $this->virtual == 'yes' ? true : false, $this );
468
	}
469
470
	/**
471
	 * Checks if a product needs shipping.
472
	 *
473
	 * @return bool
474
	 */
475
	public function needs_shipping() {
476
		return apply_filters( 'woocommerce_product_needs_shipping', $this->is_virtual() ? false : true, $this );
477
	}
478
479
	/**
480
	 * Check if a product is sold individually (no quantities).
481
	 *
482
	 * @return bool
483
	 */
484
	public function is_sold_individually() {
485
486
		$return = false;
487
488
		if ( 'yes' == $this->sold_individually ) {
489
			$return = true;
490
		}
491
492
		return apply_filters( 'woocommerce_is_sold_individually', $return, $this );
493
	}
494
495
	/**
496
	 * Returns the child product.
497
	 *
498
	 * @param mixed $child_id
499
	 * @return WC_Product|WC_Product|WC_Product_variation
500
	 */
501
	public function get_child( $child_id ) {
502
		return wc_get_product( $child_id );
503
	}
504
505
	/**
506
	 * Returns the children.
507
	 *
508
	 * @return array
509
	 */
510
	public function get_children() {
511
		return array();
512
	}
513
514
	/**
515
	 * Returns whether or not the product has any child product.
516
	 *
517
	 * @return bool
518
	 */
519
	public function has_child() {
520
		return false;
521
	}
522
523
	/**
524
	 * Returns whether or not the product post exists.
525
	 *
526
	 * @return bool
527
	 */
528
	public function exists() {
529
		return empty( $this->post ) ? false : true;
530
	}
531
532
	/**
533
	 * Returns whether or not the product is taxable.
534
	 *
535
	 * @return bool
536
	 */
537
	public function is_taxable() {
538
		$taxable = $this->get_tax_status() === 'taxable' && wc_tax_enabled() ? true : false;
539
		return apply_filters( 'woocommerce_product_is_taxable', $taxable, $this );
540
	}
541
542
	/**
543
	 * Returns whether or not the product shipping is taxable.
544
	 *
545
	 * @return bool
546
	 */
547
	public function is_shipping_taxable() {
548
		return $this->get_tax_status() === 'taxable' || $this->get_tax_status() === 'shipping' ? true : false;
549
	}
550
551
	/**
552
	 * Get the title of the post.
553
	 *
554
	 * @return string
555
	 */
556
	public function get_title() {
557
		return apply_filters( 'woocommerce_product_title', $this->post ? $this->post->post_title : '', $this );
558
	}
559
560
	/**
561
	 * Get the parent of the post.
562
	 *
563
	 * @return int
564
	 */
565
	public function get_parent() {
566
		return apply_filters( 'woocommerce_product_parent', absint( $this->post->post_parent ), $this );
567
	}
568
569
	/**
570
	 * Get the add to url used mainly in loops.
571
	 *
572
	 * @return string
573
	 */
574
	public function add_to_cart_url() {
575
		return apply_filters( 'woocommerce_product_add_to_cart_url', get_permalink( $this->id ), $this );
576
	}
577
578
	/**
579
	 * Get the add to cart button text for the single page.
580
	 *
581
	 * @return string
582
	 */
583
	public function single_add_to_cart_text() {
584
		return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this );
585
	}
586
587
	/**
588
	 * Get the add to cart button text.
589
	 *
590
	 * @return string
591
	 */
592
	public function add_to_cart_text() {
593
		return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this );
594
	}
595
596
	/**
597
	 * Returns whether or not the product is stock managed.
598
	 *
599
	 * @return bool
600
	 */
601
	public function managing_stock() {
602
		return ( ! isset( $this->manage_stock ) || $this->manage_stock == 'no' || get_option( 'woocommerce_manage_stock' ) !== 'yes' ) ? false : true;
603
	}
604
605
	/**
606
	 * Returns whether or not the product is in stock.
607
	 *
608
	 * @return bool
609
	 */
610 View Code Duplication
	public function is_in_stock() {
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...
611
		$status = $this->stock_status === 'instock';
612
613
		/**
614
		 * Sanity check to ensure stock qty is not lower than 0 but still listed
615
		 * instock.
616
		 *
617
		 * Check is not required for products on backorder since they can be
618
		 * instock regardless of actual stock quantity.
619
		 */
620
		if ( $this->managing_stock() && ! $this->backorders_allowed() && $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
621
			$status = false;
622
		}
623
624
		return apply_filters( 'woocommerce_product_is_in_stock', $status );
625
	}
626
627
	/**
628
	 * Returns whether or not the product can be backordered.
629
	 *
630
	 * @return bool
631
	 */
632
	public function backorders_allowed() {
633
		return apply_filters( 'woocommerce_product_backorders_allowed', $this->backorders === 'yes' || $this->backorders === 'notify' ? true : false, $this->id, $this );
634
	}
635
636
	/**
637
	 * Returns whether or not the product needs to notify the customer on backorder.
638
	 *
639
	 * @return bool
640
	 */
641
	public function backorders_require_notification() {
642
		return apply_filters( 'woocommerce_product_backorders_require_notification', $this->managing_stock() && $this->backorders === 'notify' ? true : false, $this );
643
	}
644
645
	/**
646
	 * Check if a product is on backorder.
647
	 *
648
	 * @param int $qty_in_cart (default: 0)
649
	 * @return bool
650
	 */
651
	public function is_on_backorder( $qty_in_cart = 0 ) {
652
		return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_total_stock() - $qty_in_cart ) < 0 ? true : false;
653
	}
654
655
	/**
656
	 * Returns whether or not the product has enough stock for the order.
657
	 *
658
	 * @param mixed $quantity
659
	 * @return bool
660
	 */
661
	public function has_enough_stock( $quantity ) {
662
		return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity ? true : false;
663
	}
664
665
	/**
666
	 * Returns the availability of the product.
667
	 *
668
	 * If stock management is enabled at global and product level, a stock message
669
	 * will be shown. e.g. In stock, In stock x10, Out of stock.
670
	 *
671
	 * If stock management is disabled at global or product level, out of stock
672
	 * will be shown when needed, but in stock will be hidden from view.
673
	 *
674
	 * This can all be changed through use of the woocommerce_get_availability filter.
675
	 *
676
	 * @return string
677
	 */
678
	public function get_availability() {
679
		return apply_filters( 'woocommerce_get_availability', array(
680
			'availability' => $this->get_availability_text(),
681
			'class'        => $this->get_availability_class(),
682
		), $this );
683
	}
684
685
	/**
686
	 * Get availability text based on stock status.
687
	 *
688
	 * @return string
689
	 */
690
	protected function get_availability_text() {
691
		if ( ! $this->is_in_stock() ) {
692
			$availability = __( 'Out of stock', 'woocommerce' );
693
		} elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) {
694
			$availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : __( 'In stock', 'woocommerce' );
695
		} elseif ( $this->managing_stock() ) {
696
			switch ( get_option( 'woocommerce_stock_format' ) ) {
697
				case 'no_amount' :
698
					$availability = __( 'In stock', 'woocommerce' );
699
				break;
700
				case 'low_amount' :
701
					if ( $this->get_total_stock() <= get_option( 'woocommerce_notify_low_stock_amount' ) ) {
702
						$availability = sprintf( __( 'Only %s left in stock', 'woocommerce' ), $this->get_total_stock() );
703
704
						if ( $this->backorders_allowed() && $this->backorders_require_notification() ) {
705
							$availability .= ' ' . __( '(also available on backorder)', 'woocommerce' );
706
						}
707
					} else {
708
						$availability = __( 'In stock', 'woocommerce' );
709
					}
710
				break;
711
				default :
712
					$availability = sprintf( __( '%s in stock', 'woocommerce' ), $this->get_total_stock() );
713
714
					if ( $this->backorders_allowed() && $this->backorders_require_notification() ) {
715
						$availability .= ' ' . __( '(also available on backorder)', 'woocommerce' );
716
					}
717
				break;
718
			}
719
		} else {
720
			$availability = '';
721
		}
722
		return apply_filters( 'woocommerce_get_availability_text', $availability, $this );
723
	}
724
725
	/**
726
	 * Get availability classname based on stock status.
727
	 *
728
	 * @return string
729
	 */
730
	protected function get_availability_class() {
731
		if ( ! $this->is_in_stock() ) {
732
			$class = 'out-of-stock';
733
		} elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) && $this->backorders_require_notification() ) {
734
			$class = 'available-on-backorder';
735
		} else {
736
			$class = 'in-stock';
737
		}
738
		return apply_filters( 'woocommerce_get_availability_class', $class, $this );
739
	}
740
741
	/**
742
	 * Returns whether or not the product is featured.
743
	 *
744
	 * @return bool
745
	 */
746
	public function is_featured() {
747
		return $this->featured === 'yes' ? true : false;
748
	}
749
750
	/**
751
	 * Returns whether or not the product is visible in the catalog.
752
	 *
753
	 * @return bool
754
	 */
755
	public function is_visible() {
756
		if ( ! $this->post ) {
757
			$visible = false;
758
759
		// Published/private
760 View Code Duplication
		} elseif ( $this->post->post_status !== 'publish' && ! current_user_can( 'edit_post', $this->id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
761
			$visible = false;
762
763
		// Out of stock visibility
764
		} elseif ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) {
765
			$visible = false;
766
767
		// visibility setting
768
		} elseif ( 'hidden' === $this->visibility ) {
769
			$visible = false;
770
		} elseif ( 'visible' === $this->visibility ) {
771
			$visible = true;
772
773
		// Visibility in loop
774
		} elseif ( is_search() ) {
775
			$visible = 'search' === $this->visibility;
776
		} else {
777
			$visible = 'catalog' === $this->visibility;
778
		}
779
780
		return apply_filters( 'woocommerce_product_is_visible', $visible, $this->id );
781
	}
782
783
	/**
784
	 * Returns whether or not the product is on sale.
785
	 *
786
	 * @return bool
787
	 */
788
	public function is_on_sale() {
789
		return apply_filters( 'woocommerce_product_is_on_sale', ( $this->get_sale_price() !== $this->get_regular_price() && $this->get_sale_price() === $this->get_price() ), $this );
790
	}
791
792
	/**
793
	 * Returns false if the product cannot be bought.
794
	 *
795
	 * @return bool
796
	 */
797
	public function is_purchasable() {
798
799
		$purchasable = true;
800
801
		// Products must exist of course
802
		if ( ! $this->exists() ) {
803
			$purchasable = false;
804
805
		// Other products types need a price to be set
806
		} elseif ( $this->get_price() === '' ) {
807
			$purchasable = false;
808
809
		// Check the product is published
810 View Code Duplication
		} elseif ( $this->post->post_status !== 'publish' && ! current_user_can( 'edit_post', $this->id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
811
			$purchasable = false;
812
		}
813
814
		return apply_filters( 'woocommerce_is_purchasable', $purchasable, $this );
815
	}
816
817
	/**
818
	 * Set a products price dynamically.
819
	 *
820
	 * @param float $price Price to set.
821
	 */
822
	public function set_price( $price ) {
823
		$this->price = $price;
824
	}
825
826
	/**
827
	 * Adjust a products price dynamically.
828
	 *
829
	 * @param mixed $price
830
	 */
831
	public function adjust_price( $price ) {
832
		$this->price = $this->price + $price;
833
	}
834
835
	/**
836
	 * Returns the product's sale price.
837
	 *
838
	 * @return string price
839
	 */
840
	public function get_sale_price() {
841
		return apply_filters( 'woocommerce_get_sale_price', $this->sale_price, $this );
842
	}
843
844
	/**
845
	 * Returns the product's regular price.
846
	 *
847
	 * @return string price
848
	 */
849
	public function get_regular_price() {
850
		return apply_filters( 'woocommerce_get_regular_price', $this->regular_price, $this );
851
	}
852
853
	/**
854
	 * Returns the product's active price.
855
	 *
856
	 * @return string price
857
	 */
858
	public function get_price() {
859
		return apply_filters( 'woocommerce_get_price', $this->price, $this );
860
	}
861
862
	/**
863
	 * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes.
864
	 *
865
	 * @param  int $qty
866
	 * @param  string $price to calculate, left blank to just use get_price()
867
	 * @return string
868
	 */
869
	public function get_price_including_tax( $qty = 1, $price = '' ) {
870
871
		if ( $price === '' ) {
872
			$price = $this->get_price();
873
		}
874
875
		if ( $this->is_taxable() ) {
876
877
			if ( get_option( 'woocommerce_prices_include_tax' ) === 'no' ) {
878
879
				$tax_rates  = WC_Tax::get_rates( $this->get_tax_class() );
880
				$taxes      = WC_Tax::calc_tax( $price * $qty, $tax_rates, false );
881
				$tax_amount = WC_Tax::get_tax_total( $taxes );
882
				$price      = round( $price * $qty + $tax_amount, wc_get_price_decimals() );
883
884
			} else {
885
886
				$tax_rates      = WC_Tax::get_rates( $this->get_tax_class() );
887
				$base_tax_rates = WC_Tax::get_base_tax_rates( $this->tax_class );
888
889
				if ( ! empty( WC()->customer ) && WC()->customer->is_vat_exempt() ) {
890
891
					$base_taxes         = WC_Tax::calc_tax( $price * $qty, $base_tax_rates, true );
892
					$base_tax_amount    = array_sum( $base_taxes );
893
					$price              = round( $price * $qty - $base_tax_amount, wc_get_price_decimals() );
894
895
				/**
896
				 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
897
				 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
898
				 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
899
				 */
900
				} elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
901
902
					$base_taxes         = WC_Tax::calc_tax( $price * $qty, $base_tax_rates, true );
903
					$modded_taxes       = WC_Tax::calc_tax( ( $price * $qty ) - array_sum( $base_taxes ), $tax_rates, false );
904
					$price              = round( ( $price * $qty ) - array_sum( $base_taxes ) + array_sum( $modded_taxes ), wc_get_price_decimals() );
905
906
				} else {
907
908
					$price = $price * $qty;
909
910
				}
911
912
			}
913
914
		} else {
915
			$price = $price * $qty;
916
		}
917
918
		return apply_filters( 'woocommerce_get_price_including_tax', $price, $qty, $this );
919
	}
920
921
	/**
922
	 * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting.
923
	 * Uses store base tax rates. Can work for a specific $qty for more accurate taxes.
924
	 *
925
	 * @param  int $qty
926
	 * @param  string $price to calculate, left blank to just use get_price()
927
	 * @return string
928
	 */
929
	public function get_price_excluding_tax( $qty = 1, $price = '' ) {
930
931
		if ( $price === '' ) {
932
			$price = $this->get_price();
933
		}
934
935
		if ( $this->is_taxable() && 'yes' === get_option( 'woocommerce_prices_include_tax' ) ) {
936
			$tax_rates  = WC_Tax::get_base_tax_rates( $this->tax_class );
937
			$taxes      = WC_Tax::calc_tax( $price * $qty, $tax_rates, true );
938
			$price      = WC_Tax::round( $price * $qty - array_sum( $taxes ) );
939
		} else {
940
			$price = $price * $qty;
941
		}
942
943
		return apply_filters( 'woocommerce_get_price_excluding_tax', $price, $qty, $this );
944
	}
945
946
	/**
947
	 * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting.
948
	 *
949
	 * @param  string  $price to calculate, left blank to just use get_price()
950
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax()
951
	 * @return string
952
	 */
953
	public function get_display_price( $price = '', $qty = 1 ) {
954
955
		if ( $price === '' ) {
956
			$price = $this->get_price();
957
		}
958
959
		$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
960
		$display_price    = $tax_display_mode == 'incl' ? $this->get_price_including_tax( $qty, $price ) : $this->get_price_excluding_tax( $qty, $price );
961
962
		return $display_price;
963
	}
964
965
	/**
966
	 * Get the suffix to display after prices > 0.
967
	 *
968
	 * @param  string  $price to calculate, left blank to just use get_price()
969
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax()
970
	 * @return string
971
	 */
972
	public function get_price_suffix( $price = '', $qty = 1 ) {
973
974
		if ( $price === '' ) {
975
			$price = $this->get_price();
976
		}
977
978
		$price_display_suffix  = get_option( 'woocommerce_price_display_suffix' );
979
980
		if ( $price_display_suffix ) {
981
982
			$price_display_suffix = ' <small class="woocommerce-price-suffix">' . $price_display_suffix . '</small>';
983
984
			$find = array(
985
				'{price_including_tax}',
986
				'{price_excluding_tax}'
987
			);
988
989
			$replace = array(
990
				wc_price( $this->get_price_including_tax( $qty, $price ) ),
991
				wc_price( $this->get_price_excluding_tax( $qty, $price ) )
992
			);
993
994
			$price_display_suffix = str_replace( $find, $replace, $price_display_suffix );
995
		}
996
997
		return apply_filters( 'woocommerce_get_price_suffix', $price_display_suffix, $this );
998
	}
999
1000
	/**
1001
	 * Returns the price in html format.
1002
	 *
1003
	 * @param string $price (default: '')
1004
	 * @return string
1005
	 */
1006
	public function get_price_html( $price = '' ) {
1007
1008
		$display_price         = $this->get_display_price();
1009
		$display_regular_price = $this->get_display_price( $this->get_regular_price() );
1010
1011
		if ( $this->get_price() > 0 ) {
1012
1013
			if ( $this->is_on_sale() && $this->get_regular_price() ) {
1014
1015
				$price .= $this->get_price_html_from_to( $display_regular_price, $display_price ) . $this->get_price_suffix();
1016
1017
				$price = apply_filters( 'woocommerce_sale_price_html', $price, $this );
1018
1019
			} else {
1020
1021
				$price .= wc_price( $display_price ) . $this->get_price_suffix();
1022
1023
				$price = apply_filters( 'woocommerce_price_html', $price, $this );
1024
1025
			}
1026
1027
		} elseif ( $this->get_price() === '' ) {
1028
1029
			$price = apply_filters( 'woocommerce_empty_price_html', '', $this );
1030
1031
		} elseif ( $this->get_price() == 0 ) {
1032
1033
			if ( $this->is_on_sale() && $this->get_regular_price() ) {
1034
1035
				$price .= $this->get_price_html_from_to( $display_regular_price, __( 'Free!', 'woocommerce' ) );
1036
1037
				$price = apply_filters( 'woocommerce_free_sale_price_html', $price, $this );
1038
1039
			} else {
1040
1041
				$price = '<span class="amount">' . __( 'Free!', 'woocommerce' ) . '</span>';
1042
1043
				$price = apply_filters( 'woocommerce_free_price_html', $price, $this );
1044
1045
			}
1046
		}
1047
1048
		return apply_filters( 'woocommerce_get_price_html', $price, $this );
1049
	}
1050
1051
	/**
1052
	 * Functions for getting parts of a price, in html, used by get_price_html.
1053
	 *
1054
	 * @return string
1055
	 */
1056
	public function get_price_html_from_text() {
1057
		$from = '<span class="from">' . _x( 'From:', 'min_price', 'woocommerce' ) . ' </span>';
1058
1059
		return apply_filters( 'woocommerce_get_price_html_from_text', $from, $this );
1060
	}
1061
1062
	/**
1063
	 * Functions for getting parts of a price, in html, used by get_price_html.
1064
	 *
1065
	 * @param  string $from String or float to wrap with 'from' text
1066
	 * @param  mixed $to String or float to wrap with 'to' text
1067
	 * @return string
1068
	 */
1069
	public function get_price_html_from_to( $from, $to ) {
1070
		$price = '<del>' . ( ( is_numeric( $from ) ) ? wc_price( $from ) : $from ) . '</del> <ins>' . ( ( is_numeric( $to ) ) ? wc_price( $to ) : $to ) . '</ins>';
1071
1072
		return apply_filters( 'woocommerce_get_price_html_from_to', $price, $from, $to, $this );
1073
	}
1074
1075
	/**
1076
	 * Returns the tax class.
1077
	 *
1078
	 * @return string
1079
	 */
1080
	public function get_tax_class() {
1081
		return apply_filters( 'woocommerce_product_tax_class', $this->tax_class, $this );
1082
	}
1083
1084
	/**
1085
	 * Returns the tax status.
1086
	 *
1087
	 * @return string
1088
	 */
1089
	public function get_tax_status() {
1090
		return $this->tax_status;
1091
	}
1092
1093
	/**
1094
	 * Get the average rating of product. This is calculated once and stored in postmeta.
1095
	 * @return string
1096
	 */
1097
	public function get_average_rating() {
1098
		// No meta data? Do the calculation
1099
		if ( ! metadata_exists( 'post', $this->id, '_wc_average_rating' ) ) {
1100
			$this->sync_average_rating( $this->id );
1101
		}
1102
1103
		return (string) floatval( get_post_meta( $this->id, '_wc_average_rating', true ) );
1104
	}
1105
1106
	/**
1107
	 * Get the total amount (COUNT) of ratings.
1108
	 * @param  int $value Optional. Rating value to get the count for. By default returns the count of all rating values.
1109
	 * @return int
1110
	 */
1111
	public function get_rating_count( $value = null ) {
1112
		// No meta data? Do the calculation
1113
		if ( ! metadata_exists( 'post', $this->id, '_wc_rating_count' ) ) {
1114
			$this->sync_rating_count( $this->id );
1115
		}
1116
1117
		$counts = get_post_meta( $this->id, '_wc_rating_count', true );
1118
1119
		if ( is_null( $value ) ) {
1120
			return array_sum( $counts );
1121
		} else {
1122
			return isset( $counts[ $value ] ) ? $counts[ $value ] : 0;
1123
		}
1124
	}
1125
1126
	/**
1127
	 * Sync product rating. Can be called statically.
1128
	 * @param  int $post_id
1129
	 */
1130
	public static function sync_average_rating( $post_id ) {
1131
		if ( ! metadata_exists( 'post', $post_id, '_wc_rating_count' ) ) {
1132
			self::sync_rating_count( $post_id );
1133
		}
1134
1135
		$count = array_sum( (array) get_post_meta( $post_id, '_wc_rating_count', true ) );
1136
1137
		if ( $count ) {
1138
			global $wpdb;
1139
1140
			$ratings = $wpdb->get_var( $wpdb->prepare("
1141
				SELECT SUM(meta_value) FROM $wpdb->commentmeta
1142
				LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
1143
				WHERE meta_key = 'rating'
1144
				AND comment_post_ID = %d
1145
				AND comment_approved = '1'
1146
				AND meta_value > 0
1147
			", $post_id ) );
1148
			$average = number_format( $ratings / $count, 2, '.', '' );
1149
		} else {
1150
			$average = 0;
1151
		}
1152
		update_post_meta( $post_id, '_wc_average_rating', $average );
1153
	}
1154
1155
	/**
1156
	 * Sync product rating count. Can be called statically.
1157
	 * @param  int $post_id
1158
	 */
1159
	public static function sync_rating_count( $post_id ) {
1160
		global $wpdb;
1161
1162
		$counts     = array();
1163
		$raw_counts = $wpdb->get_results( $wpdb->prepare( "
1164
			SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta
1165
			LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
1166
			WHERE meta_key = 'rating'
1167
			AND comment_post_ID = %d
1168
			AND comment_approved = '1'
1169
			AND meta_value > 0
1170
			GROUP BY meta_value
1171
		", $post_id ) );
1172
1173
		foreach ( $raw_counts as $count ) {
1174
			$counts[ $count->meta_value ] = $count->meta_value_count;
1175
		}
1176
1177
		update_post_meta( $post_id, '_wc_rating_count', $counts );
1178
	}
1179
1180
	/**
1181
	 * Returns the product rating in html format.
1182
	 *
1183
	 * @param string $rating (default: '')
1184
	 *
1185
	 * @return string
1186
	 */
1187
	public function get_rating_html( $rating = null ) {
1188
		$rating_html = '';
1189
1190
		if ( ! is_numeric( $rating ) ) {
1191
			$rating = $this->get_average_rating();
1192
		}
1193
1194
		if ( $rating > 0 ) {
1195
1196
			$rating_html  = '<div class="star-rating" title="' . sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ) . '">';
1197
1198
			$rating_html .= '<span style="width:' . ( ( $rating / 5 ) * 100 ) . '%"><strong class="rating">' . $rating . '</strong> ' . __( 'out of 5', 'woocommerce' ) . '</span>';
1199
1200
			$rating_html .= '</div>';
1201
		}
1202
1203
		return apply_filters( 'woocommerce_product_get_rating_html', $rating_html, $rating );
1204
	}
1205
1206
	/**
1207
	 * Get the total amount (COUNT) of reviews.
1208
	 *
1209
	 * @since 2.3.2
1210
	 * @return int The total numver of product reviews
1211
	 */
1212
	public function get_review_count() {
1213
		global $wpdb;
1214
1215
		// No meta date? Do the calculation
1216
		if ( ! metadata_exists( 'post', $this->id, '_wc_review_count' ) ) {
1217
			$count = $wpdb->get_var( $wpdb->prepare("
1218
				SELECT COUNT(*) FROM $wpdb->comments
1219
				WHERE comment_parent = 0
1220
				AND comment_post_ID = %d
1221
				AND comment_approved = '1'
1222
			", $this->id ) );
1223
1224
			update_post_meta( $this->id, '_wc_review_count', $count );
1225
		} else {
1226
			$count = get_post_meta( $this->id, '_wc_review_count', true );
1227
		}
1228
1229
		return apply_filters( 'woocommerce_product_review_count', $count, $this );
1230
	}
1231
1232
	/**
1233
	 * Returns the upsell product ids.
1234
	 *
1235
	 * @return array
1236
	 */
1237
	public function get_upsells() {
1238
		return apply_filters( 'woocommerce_product_upsell_ids', (array) maybe_unserialize( $this->upsell_ids ), $this );
1239
	}
1240
1241
	/**
1242
	 * Returns the cross sell product ids.
1243
	 *
1244
	 * @return array
1245
	 */
1246
	public function get_cross_sells() {
1247
		return apply_filters( 'woocommerce_product_crosssell_ids', (array) maybe_unserialize( $this->crosssell_ids ), $this );
1248
	}
1249
1250
	/**
1251
	 * Returns the product categories.
1252
	 *
1253
	 * @param string $sep (default: ', ')
1254
	 * @param string $before (default: '')
1255
	 * @param string $after (default: '')
1256
	 * @return string
1257
	 */
1258
	public function get_categories( $sep = ', ', $before = '', $after = '' ) {
1259
		return get_the_term_list( $this->id, 'product_cat', $before, $sep, $after );
1260
	}
1261
1262
	/**
1263
	 * Returns the product tags.
1264
	 *
1265
	 * @param string $sep (default: ', ')
1266
	 * @param string $before (default: '')
1267
	 * @param string $after (default: '')
1268
	 * @return array
1269
	 */
1270
	public function get_tags( $sep = ', ', $before = '', $after = '' ) {
1271
		return get_the_term_list( $this->id, 'product_tag', $before, $sep, $after );
1272
	}
1273
1274
	/**
1275
	 * Returns the product shipping class.
1276
	 *
1277
	 * @return string
1278
	 */
1279 View Code Duplication
	public function get_shipping_class() {
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...
1280
1281
		if ( ! $this->shipping_class ) {
1282
1283
			$classes = get_the_terms( $this->id, 'product_shipping_class' );
1284
1285
			if ( $classes && ! is_wp_error( $classes ) ) {
1286
				$this->shipping_class = current( $classes )->slug;
1287
			} else {
1288
				$this->shipping_class = '';
1289
			}
1290
1291
		}
1292
1293
		return $this->shipping_class;
1294
	}
1295
1296
	/**
1297
	 * Returns the product shipping class ID.
1298
	 *
1299
	 * @return int
1300
	 */
1301 View Code Duplication
	public function get_shipping_class_id() {
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...
1302
1303
		if ( ! $this->shipping_class_id ) {
1304
1305
			$classes = get_the_terms( $this->id, 'product_shipping_class' );
1306
1307
			if ( $classes && ! is_wp_error( $classes ) ) {
1308
				$this->shipping_class_id = current( $classes )->term_id;
1309
			} else {
1310
				$this->shipping_class_id = 0;
1311
			}
1312
		}
1313
1314
		return absint( $this->shipping_class_id );
1315
	}
1316
1317
	/**
1318
	 * Get and return related products.
1319
	 *
1320
	 * Notes:
1321
	 * 	- Results are cached in a transient for faster queries.
1322
	 *  - To make results appear random, we query and extra 10 products and shuffle them.
1323
	 *  - To ensure we always have enough results, it will check $limit before returning the cached result, if not recalc.
1324
	 *  - This used to rely on transient version to invalidate cache, but to avoid multiple transients we now just expire daily.
1325
	 *  	This means if a related product is edited and no longer related, it won't be removed for 24 hours. Acceptable trade-off for performance.
1326
	 *  - Saving a product will flush caches for that product.
1327
	 *
1328
	 * @param int $limit (default: 5) Should be an integer greater than 0.
1329
	 * @return array Array of post IDs
1330
	 */
1331
	public function get_related( $limit = 5 ) {
1332
		global $wpdb;
1333
1334
		$transient_name = 'wc_related_' . $this->id;
1335
		$related_posts  = get_transient( $transient_name );
1336
		$limit          = $limit > 0 ? $limit : 5;
1337
1338
		// We want to query related posts if they are not cached, or we don't have enough
1339
		if ( false === $related_posts || sizeof( $related_posts ) < $limit ) {
1340
			// Related products are found from category and tag
1341
			$tags_array = $this->get_related_terms( 'product_tag' );
1342
			$cats_array = $this->get_related_terms( 'product_cat' );
1343
1344
			// Don't bother if none are set
1345
			if ( 1 === sizeof( $cats_array ) && 1 === sizeof( $tags_array )) {
1346
				$related_posts = array();
1347
			} else {
1348
				// Sanitize
1349
				$exclude_ids = array_map( 'absint', array_merge( array( 0, $this->id ), $this->get_upsells() ) );
1350
1351
				// Generate query - but query an extra 10 results to give the appearance of random results
1352
				$query = $this->build_related_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 );
1353
1354
				// Get the posts
1355
				$related_posts = $wpdb->get_col( implode( ' ', $query ) );
1356
			}
1357
1358
			set_transient( $transient_name, $related_posts, DAY_IN_SECONDS );
1359
		}
1360
1361
		// Randomise the results
1362
		shuffle( $related_posts );
1363
1364
		// Limit the returned results
1365
		return array_slice( $related_posts, 0, $limit );
1366
	}
1367
1368
	/**
1369
	 * Returns a single product attribute.
1370
	 *
1371
	 * @param mixed $attr
1372
	 * @return string
1373
	 */
1374
	public function get_attribute( $attr ) {
1375
1376
		$attributes = $this->get_attributes();
1377
1378
		$attr = sanitize_title( $attr );
1379
1380
		if ( isset( $attributes[ $attr ] ) || isset( $attributes[ 'pa_' . $attr ] ) ) {
1381
1382
			$attribute = isset( $attributes[ $attr ] ) ? $attributes[ $attr ] : $attributes[ 'pa_' . $attr ];
1383
1384
			if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
1385
1386
				return implode( ', ', wc_get_product_terms( $this->id, $attribute['name'], array( 'fields' => 'names' ) ) );
1387
1388
			} else {
1389
1390
				return $attribute['value'];
1391
			}
1392
1393
		}
1394
1395
		return '';
1396
	}
1397
1398
	/**
1399
	 * Returns product attributes.
1400
	 *
1401
	 * @return array
1402
	 */
1403
	public function get_attributes() {
1404
		$attributes = array_filter( (array) maybe_unserialize( $this->product_attributes ) );
1405
		$taxonomies = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_name' );
1406
1407
		// Check for any attributes which have been removed globally
1408
		foreach ( $attributes as $key => $attribute ) {
1409
			if ( $attribute['is_taxonomy'] ) {
1410
				if ( ! in_array( substr( $attribute['name'], 3 ), $taxonomies ) ) {
1411
					unset( $attributes[ $key ] );
1412
				}
1413
			}
1414
		}
1415
1416
		return apply_filters( 'woocommerce_get_product_attributes', $attributes );
1417
	}
1418
1419
	/**
1420
	 * Returns whether or not the product has any attributes set.
1421
	 *
1422
	 * @return boolean
1423
	 */
1424
	public function has_attributes() {
1425
1426
		if ( sizeof( $this->get_attributes() ) > 0 ) {
1427
1428
			foreach ( $this->get_attributes() as $attribute ) {
1429
1430
				if ( isset( $attribute['is_visible'] ) && $attribute['is_visible'] ) {
1431
					return true;
1432
				}
1433
			}
1434
		}
1435
1436
		return false;
1437
	}
1438
1439
	/**
1440
	 * Returns whether or not we are showing dimensions on the product page.
1441
	 *
1442
	 * @return bool
1443
	 */
1444
	public function enable_dimensions_display() {
1445
		return apply_filters( 'wc_product_enable_dimensions_display', true );
1446
	}
1447
1448
	/**
1449
	 * Returns whether or not the product has dimensions set.
1450
	 *
1451
	 * @return bool
1452
	 */
1453
	public function has_dimensions() {
1454
		return $this->get_dimensions() ? true : false;
1455
	}
1456
1457
	/**
1458
	 * Returns the product length.
1459
	 * @return string
1460
	 */
1461
	public function get_length() {
1462
		return apply_filters( 'woocommerce_product_length', $this->length ? $this->length : '', $this );
1463
	}
1464
1465
	/**
1466
	 * Returns the product width.
1467
	 * @return string
1468
	 */
1469
	public function get_width() {
1470
		return apply_filters( 'woocommerce_product_width', $this->width ? $this->width : '', $this );
1471
	}
1472
1473
	/**
1474
	 * Returns the product height.
1475
	 * @return string
1476
	 */
1477
	public function get_height() {
1478
		return apply_filters( 'woocommerce_product_height', $this->height ? $this->height : '', $this );
1479
	}
1480
1481
	/**
1482
	 * Returns the product's weight.
1483
	 * @todo   refactor filters in this class to naming woocommerce_product_METHOD
1484
	 * @return string
1485
	 */
1486
	public function get_weight() {
1487
		return apply_filters( 'woocommerce_product_weight', apply_filters( 'woocommerce_product_get_weight', $this->weight ? $this->weight : '' ), $this );
1488
	}
1489
1490
	/**
1491
	 * Returns whether or not the product has weight set.
1492
	 *
1493
	 * @return bool
1494
	 */
1495
	public function has_weight() {
1496
		return $this->get_weight() ? true : false;
1497
	}
1498
1499
	/**
1500
	 * Returns formatted dimensions.
1501
	 * @return string
1502
	 */
1503
	public function get_dimensions() {
1504
		$dimensions = implode( ' x ', array_filter( array(
1505
			wc_format_localized_decimal( $this->get_length() ),
1506
			wc_format_localized_decimal( $this->get_width() ),
1507
			wc_format_localized_decimal( $this->get_height() ),
1508
		) ) );
1509
1510
		if ( ! empty( $dimensions ) ) {
1511
			$dimensions .= ' ' . get_option( 'woocommerce_dimension_unit' );
1512
		}
1513
1514
		return  apply_filters( 'woocommerce_product_dimensions', $dimensions, $this );
1515
	}
1516
1517
	/**
1518
	 * Lists a table of attributes for the product page.
1519
	 */
1520
	public function list_attributes() {
1521
		wc_get_template( 'single-product/product-attributes.php', array(
1522
			'product'    => $this
1523
		) );
1524
	}
1525
1526
	/**
1527
	 * Gets the main product image ID.
1528
	 *
1529
	 * @return int
1530
	 */
1531
	public function get_image_id() {
1532
1533
		if ( has_post_thumbnail( $this->id ) ) {
1534
			$image_id = get_post_thumbnail_id( $this->id );
1535 View Code Duplication
		} elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1536
			$image_id = get_post_thumbnail_id( $parent_id );
1537
		} else {
1538
			$image_id = 0;
1539
		}
1540
1541
		return $image_id;
1542
	}
1543
1544
	/**
1545
	 * Returns the main product image.
1546
	 *
1547
	 * @param string $size (default: 'shop_thumbnail')
1548
	 * @param array $attr
1549
	 * @param bool True to return $placeholder if no image is found, or false to return an empty string.
1550
	 * @return string
1551
	 */
1552
	public function get_image( $size = 'shop_thumbnail', $attr = array(), $placeholder = true ) {
1553
		if ( has_post_thumbnail( $this->id ) ) {
1554
			$image = get_the_post_thumbnail( $this->id, $size, $attr );
1555 View Code Duplication
		} elseif ( ( $parent_id = wp_get_post_parent_id( $this->id ) ) && has_post_thumbnail( $parent_id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1556
			$image = get_the_post_thumbnail( $parent_id, $size, $attr );
1557
		} elseif ( $placeholder ) {
1558
			$image = wc_placeholder_img( $size );
1559
		} else {
1560
			$image = '';
1561
		}
1562
1563
		return $image;
1564
	}
1565
1566
	/**
1567
	 * Get product name with SKU or ID. Used within admin.
1568
	 *
1569
	 * @return string Formatted product name
1570
	 */
1571
	public function get_formatted_name() {
1572
		if ( $this->get_sku() ) {
1573
			$identifier = $this->get_sku();
1574
		} else {
1575
			$identifier = '#' . $this->id;
1576
		}
1577
1578
		return sprintf( '%s &ndash; %s', $identifier, $this->get_title() );
1579
	}
1580
1581
	/**
1582
	 * Retrieves related product terms.
1583
	 *
1584
	 * @param string $term
1585
	 * @return array
1586
	 */
1587
	protected function get_related_terms( $term ) {
1588
		$terms_array = array(0);
1589
1590
		$terms = apply_filters( 'woocommerce_get_related_' . $term . '_terms', wp_get_post_terms( $this->id, $term ), $this->id );
1591
		foreach ( $terms as $term ) {
1592
			$terms_array[] = $term->term_id;
1593
		}
1594
1595
		return array_map( 'absint', $terms_array );
1596
	}
1597
1598
	/**
1599
	 * Builds the related posts query.
1600
	 *
1601
	 * @param array $cats_array
1602
	 * @param array $tags_array
1603
	 * @param array $exclude_ids
1604
	 * @param int   $limit
1605
	 * @return string
1606
	 */
1607
	protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
1608
		global $wpdb;
1609
1610
		$limit = absint( $limit );
1611
1612
		$query           = array();
1613
		$query['fields'] = "SELECT DISTINCT ID FROM {$wpdb->posts} p";
1614
		$query['join']   = " INNER JOIN {$wpdb->postmeta} pm ON ( pm.post_id = p.ID AND pm.meta_key='_visibility' )";
1615
		$query['join']  .= " INNER JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id)";
1616
		$query['join']  .= " INNER JOIN {$wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
1617
		$query['join']  .= " INNER JOIN {$wpdb->terms} t ON (t.term_id = tt.term_id)";
1618
1619
		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1620
			$query['join'] .= " INNER JOIN {$wpdb->postmeta} pm2 ON ( pm2.post_id = p.ID AND pm2.meta_key='_stock_status' )";
1621
		}
1622
1623
		$query['where']  = " WHERE 1=1";
1624
		$query['where'] .= " AND p.post_status = 'publish'";
1625
		$query['where'] .= " AND p.post_type = 'product'";
1626
		$query['where'] .= " AND p.ID NOT IN ( " . implode( ',', $exclude_ids ) . " )";
1627
		$query['where'] .= " AND pm.meta_value IN ( 'visible', 'catalog' )";
1628
1629
		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1630
			$query['where'] .= " AND pm2.meta_value = 'instock'";
1631
		}
1632
1633
		$relate_by_category = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $this->id );
1634
		$relate_by_tag      = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $this->id );
1635
1636
		if ( $relate_by_category || $relate_by_tag ) {
1637
			$query['where'] .= ' AND (';
1638
1639
			if ( $relate_by_category ) {
1640
				$query['where'] .= " ( tt.taxonomy = 'product_cat' AND t.term_id IN ( " . implode( ',', $cats_array ) . " ) ) ";
1641
				if ( $relate_by_tag ) {
1642
					$query['where'] .= ' OR ';
1643
				}
1644
			}
1645
1646
			if ( $relate_by_tag ) {
1647
				$query['where'] .= " ( tt.taxonomy = 'product_tag' AND t.term_id IN ( " . implode( ',', $tags_array ) . " ) ) ";
1648
			}
1649
1650
			$query['where'] .= ')';
1651
		}
1652
1653
		$query['limits'] = " LIMIT {$limit} ";
1654
		$query           = apply_filters( 'woocommerce_product_related_posts_query', $query, $this->id );
1655
1656
		return $query;
1657
	}
1658
}
1659