Completed
Pull Request — master (#10259)
by Mike
13:41
created

WC_Product::is_featured()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

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