Completed
Pull Request — master (#11971)
by Aristeides
07:37
created

WC_Product::is_visible()   C

Complexity

Conditions 9
Paths 7

Size

Total Lines 23
Code Lines 16

Duplication

Lines 5
Ratio 21.74 %

Importance

Changes 0
Metric Value
cc 9
eloc 16
nc 7
nop 0
dl 5
loc 23
rs 5.8541
c 0
b 0
f 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 - 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 ( 'outofstock' !== $this->stock_status ) {
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 ( 'instock' !== $this->stock_status ) {
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 ( 'yes' === $this->downloadable );
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', ( 'yes' === $this->virtual ), $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 ) || 'no' === $this->manage_stock || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) ? false : true;
603
	}
604
605
	/**
606
	 * Returns whether or not the product is in stock.
607
	 *
608
	 * @return bool
609
	 */
610
	public function is_in_stock() {
611
		return apply_filters( 'woocommerce_product_is_in_stock', ( 'instock' === $this->stock_status ), $this );
612
	}
613
614
	/**
615
	 * Returns whether or not the product can be backordered.
616
	 *
617
	 * @return bool
618
	 */
619
	public function backorders_allowed() {
620
		return apply_filters( 'woocommerce_product_backorders_allowed', ( 'yes' === $this->backorders || 'notify' === $this->backorders ), $this->id, $this );
621
	}
622
623
	/**
624
	 * Returns whether or not the product needs to notify the customer on backorder.
625
	 *
626
	 * @return bool
627
	 */
628
	public function backorders_require_notification() {
629
		return apply_filters( 'woocommerce_product_backorders_require_notification', ( $this->managing_stock() && 'notify' === $this->backorders ), $this );
630
	}
631
632
	/**
633
	 * Check if a product is on backorder.
634
	 *
635
	 * @param int $qty_in_cart (default: 0)
636
	 * @return bool
637
	 */
638
	public function is_on_backorder( $qty_in_cart = 0 ) {
639
		return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_total_stock() - $qty_in_cart ) < 0 ? true : false;
640
	}
641
642
	/**
643
	 * Returns whether or not the product has enough stock for the order.
644
	 *
645
	 * @param mixed $quantity
646
	 * @return bool
647
	 */
648
	public function has_enough_stock( $quantity ) {
649
		return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity ? true : false;
650
	}
651
652
	/**
653
	 * Returns the availability of the product.
654
	 *
655
	 * If stock management is enabled at global and product level, a stock message
656
	 * will be shown. e.g. In stock, In stock x10, Out of stock.
657
	 *
658
	 * If stock management is disabled at global or product level, out of stock
659
	 * will be shown when needed, but in stock will be hidden from view.
660
	 *
661
	 * This can all be changed through use of the woocommerce_get_availability filter.
662
	 *
663
	 * @return string
664
	 */
665
	public function get_availability() {
666
		return apply_filters( 'woocommerce_get_availability', array(
667
			'availability' => $this->get_availability_text(),
668
			'class'        => $this->get_availability_class(),
669
		), $this );
670
	}
671
672
	/**
673
	 * Get availability text based on stock status.
674
	 *
675
	 * @return string
676
	 */
677
	protected function get_availability_text() {
678
		if ( ! $this->is_in_stock() ) {
679
			$availability = __( 'Out of stock', 'woocommerce' );
680
		} elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) ) {
681
			$availability = $this->backorders_require_notification() ? __( 'Available on backorder', 'woocommerce' ) : __( 'In stock', 'woocommerce' );
682
		} elseif ( $this->managing_stock() ) {
683
			switch ( get_option( 'woocommerce_stock_format' ) ) {
684
				case 'no_amount' :
685
					$availability = __( 'In stock', 'woocommerce' );
686
				break;
687
				case 'low_amount' :
688
					if ( $this->get_total_stock() <= get_option( 'woocommerce_notify_low_stock_amount' ) ) {
689
						$availability = sprintf( __( 'Only %s left in stock', 'woocommerce' ), $this->get_total_stock() );
690
691
						if ( $this->backorders_allowed() && $this->backorders_require_notification() ) {
692
							$availability .= ' ' . __( '(also available on backorder)', 'woocommerce' );
693
						}
694
					} else {
695
						$availability = __( 'In stock', 'woocommerce' );
696
					}
697
				break;
698
				default :
699
					$availability = sprintf( __( '%s in stock', 'woocommerce' ), $this->get_total_stock() );
700
701
					if ( $this->backorders_allowed() && $this->backorders_require_notification() ) {
702
						$availability .= ' ' . __( '(also available on backorder)', 'woocommerce' );
703
					}
704
				break;
705
			}
706
		} else {
707
			$availability = '';
708
		}
709
		return apply_filters( 'woocommerce_get_availability_text', $availability, $this );
710
	}
711
712
	/**
713
	 * Get availability classname based on stock status.
714
	 *
715
	 * @return string
716
	 */
717
	protected function get_availability_class() {
718
		if ( ! $this->is_in_stock() ) {
719
			$class = 'out-of-stock';
720
		} elseif ( $this->managing_stock() && $this->is_on_backorder( 1 ) && $this->backorders_require_notification() ) {
721
			$class = 'available-on-backorder';
722
		} else {
723
			$class = 'in-stock';
724
		}
725
		return apply_filters( 'woocommerce_get_availability_class', $class, $this );
726
	}
727
728
	/**
729
	 * Returns whether or not the product is featured.
730
	 *
731
	 * @return bool
732
	 */
733
	public function is_featured() {
734
		return ( 'yes' === $this->featured ) ? true : false;
735
	}
736
737
	/**
738
	 * Returns whether or not the product is visible in the catalog.
739
	 *
740
	 * @return bool
741
	 */
742
	public function is_visible() {
743
		if ( ! $this->post ) {
744
			$visible = false;
745 View Code Duplication
		} elseif ( 'publish' !== $this->post->post_status && ! 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...
746
			// Published/private.
747
			$visible = false;
748
		} elseif ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $this->is_in_stock() ) {
749
			// Out of stock visibility.
750
			$visible = false;
751
		} elseif ( 'hidden' === $this->visibility ) {
752
			// visibility setting.
753
			$visible = false;
754
		} elseif ( 'visible' === $this->visibility ) {
755
			$visible = true;
756
		} elseif ( is_search() ) {
757
			// Visibility in loop.
758
			$visible = 'search' === $this->visibility;
759
		} else {
760
			$visible = 'catalog' === $this->visibility;
761
		}
762
763
		return apply_filters( 'woocommerce_product_is_visible', $visible, $this->id );
764
	}
765
766
	/**
767
	 * Returns whether or not the product is on sale.
768
	 *
769
	 * @return bool
770
	 */
771
	public function is_on_sale() {
772
		return apply_filters( 'woocommerce_product_is_on_sale', ( $this->get_sale_price() !== $this->get_regular_price() && $this->get_sale_price() === $this->get_price() ), $this );
773
	}
774
775
	/**
776
	 * Returns false if the product cannot be bought.
777
	 *
778
	 * @return bool
779
	 */
780
	public function is_purchasable() {
781
782
		$purchasable = true;
783
784
		if ( ! $this->exists() ) {
785
			// Products must exist of course.
786
			$purchasable = false;
787
788
		} elseif ( $this->get_price() === '' ) {
789
			// Other products types need a price to be set.
790
			$purchasable = false;
791
792 View Code Duplication
		} elseif ( 'publish' !== $this->post->post_status && ! 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...
793
			// Check the product is published.
794
			$purchasable = false;
795
		}
796
797
		return apply_filters( 'woocommerce_is_purchasable', $purchasable, $this );
798
	}
799
800
	/**
801
	 * Set a products price dynamically.
802
	 *
803
	 * @param float $price Price to set.
804
	 */
805
	public function set_price( $price ) {
806
		$this->price = $price;
807
	}
808
809
	/**
810
	 * Adjust a products price dynamically.
811
	 *
812
	 * @param mixed $price
813
	 */
814
	public function adjust_price( $price ) {
815
		$this->price = $this->price + $price;
816
	}
817
818
	/**
819
	 * Returns the product's sale price.
820
	 *
821
	 * @return string price
822
	 */
823
	public function get_sale_price() {
824
		return apply_filters( 'woocommerce_get_sale_price', $this->sale_price, $this );
825
	}
826
827
	/**
828
	 * Returns the product's regular price.
829
	 *
830
	 * @return string price
831
	 */
832
	public function get_regular_price() {
833
		return apply_filters( 'woocommerce_get_regular_price', $this->regular_price, $this );
834
	}
835
836
	/**
837
	 * Returns the product's active price.
838
	 *
839
	 * @return string price
840
	 */
841
	public function get_price() {
842
		return apply_filters( 'woocommerce_get_price', $this->price, $this );
843
	}
844
845
	/**
846
	 * Returns the price (including tax). Uses customer tax rates. Can work for a specific $qty for more accurate taxes.
847
	 *
848
	 * @param  int $qty
849
	 * @param  string $price to calculate, left blank to just use get_price()
850
	 * @return string
851
	 */
852
	public function get_price_including_tax( $qty = 1, $price = '' ) {
853
854
		if ( '' === $price ) {
855
			$price = $this->get_price();
856
		}
857
858
		if ( $this->is_taxable() ) {
859
860
			if ( get_option( 'woocommerce_prices_include_tax' ) === 'no' ) {
861
862
				$tax_rates  = WC_Tax::get_rates( $this->get_tax_class() );
863
				$taxes      = WC_Tax::calc_tax( $price * $qty, $tax_rates, false );
864
				$tax_amount = WC_Tax::get_tax_total( $taxes );
865
				$price      = round( $price * $qty + $tax_amount, wc_get_price_decimals() );
866
867
			} else {
868
869
				$tax_rates      = WC_Tax::get_rates( $this->get_tax_class() );
870
				$base_tax_rates = WC_Tax::get_base_tax_rates( $this->tax_class );
871
872
				if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) {
873
874
					$base_taxes         = WC_Tax::calc_tax( $price * $qty, $base_tax_rates, true );
875
					$base_tax_amount    = array_sum( $base_taxes );
876
					$price              = round( $price * $qty - $base_tax_amount, wc_get_price_decimals() );
877
878
				} elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) {
879
					/**
880
					 * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations.
881
					 * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes.
882
					 * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk.
883
					 */
884
885
					$base_taxes         = WC_Tax::calc_tax( $price * $qty, $base_tax_rates, true );
886
					$modded_taxes       = WC_Tax::calc_tax( ( $price * $qty ) - array_sum( $base_taxes ), $tax_rates, false );
887
					$price              = round( ( $price * $qty ) - array_sum( $base_taxes ) + array_sum( $modded_taxes ), wc_get_price_decimals() );
888
889
				} else {
890
891
					$price = $price * $qty;
892
893
				}
894
			}
895
		} else {
896
			$price = $price * $qty;
897
		}
898
899
		return apply_filters( 'woocommerce_get_price_including_tax', $price, $qty, $this );
900
	}
901
902
	/**
903
	 * Returns the price (excluding tax) - ignores tax_class filters since the price may *include* tax and thus needs subtracting.
904
	 * Uses store base tax rates. Can work for a specific $qty for more accurate taxes.
905
	 *
906
	 * @param  int $qty
907
	 * @param  string $price to calculate, left blank to just use get_price()
908
	 * @return string
909
	 */
910
	public function get_price_excluding_tax( $qty = 1, $price = '' ) {
911
912
		if ( '' === $price ) {
913
			$price = $this->get_price();
914
		}
915
916
		if ( $this->is_taxable() && 'yes' === get_option( 'woocommerce_prices_include_tax' ) ) {
917
			$tax_rates  = WC_Tax::get_base_tax_rates( $this->tax_class );
918
			$taxes      = WC_Tax::calc_tax( $price * $qty, $tax_rates, true );
919
			$price      = WC_Tax::round( $price * $qty - array_sum( $taxes ) );
920
		} else {
921
			$price = $price * $qty;
922
		}
923
924
		return apply_filters( 'woocommerce_get_price_excluding_tax', $price, $qty, $this );
925
	}
926
927
	/**
928
	 * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting.
929
	 *
930
	 * @param  string  $price to calculate, left blank to just use get_price()
931
	 * @param  integer $qty   passed on to get_price_including_tax() or get_price_excluding_tax()
932
	 * @return string
933
	 */
934
	public function get_display_price( $price = '', $qty = 1 ) {
935
936
		if ( '' === $price ) {
937
			$price = $this->get_price();
938
		}
939
940
		$tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
941
		$display_price    = ( 'incl' === $tax_display_mode ) ? $this->get_price_including_tax( $qty, $price ) : $this->get_price_excluding_tax( $qty, $price );
942
943
		return $display_price;
944
	}
945
946
	/**
947
	 * Get the suffix to display after prices > 0.
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_price_suffix( $price = '', $qty = 1 ) {
954
955
		if ( '' === $price ) {
956
			$price = $this->get_price();
957
		}
958
959
		$price_display_suffix  = get_option( 'woocommerce_price_display_suffix' );
960
		$woocommerce_calc_taxes = get_option( 'woocommerce_calc_taxes', 'no' );
961
962
		if ( $price_display_suffix && 'yes' === $woocommerce_calc_taxes ) {
963
964
			$price_display_suffix = ' <small class="woocommerce-price-suffix">' . $price_display_suffix . '</small>';
965
966
			$find = array(
967
				'{price_including_tax}',
968
				'{price_excluding_tax}',
969
			);
970
971
			$replace = array(
972
				wc_price( $this->get_price_including_tax( $qty, $price ) ),
973
				wc_price( $this->get_price_excluding_tax( $qty, $price ) ),
974
			);
975
976
			$price_display_suffix = str_replace( $find, $replace, $price_display_suffix );
977
		} else {
978
			$price_display_suffix = '';
979
		}
980
981
		return apply_filters( 'woocommerce_get_price_suffix', $price_display_suffix, $this );
982
	}
983
984
	/**
985
	 * Returns the price in html format.
986
	 *
987
	 * @param string $price (default: '')
988
	 * @return string
989
	 */
990
	public function get_price_html( $price = '' ) {
991
992
		$display_price         = $this->get_display_price();
993
		$display_regular_price = $this->get_display_price( $this->get_regular_price() );
994
995
		if ( $this->get_price() > 0 ) {
996
997
			if ( $this->is_on_sale() && $this->get_regular_price() ) {
998
999
				$price .= $this->get_price_html_from_to( $display_regular_price, $display_price ) . $this->get_price_suffix();
1000
1001
				$price = apply_filters( 'woocommerce_sale_price_html', $price, $this );
1002
1003
			} else {
1004
1005
				$price .= wc_price( $display_price ) . $this->get_price_suffix();
1006
1007
				$price = apply_filters( 'woocommerce_price_html', $price, $this );
1008
1009
			}
1010
		} elseif ( $this->get_price() === '' ) {
1011
1012
			$price = apply_filters( 'woocommerce_empty_price_html', '', $this );
1013
1014
		} elseif ( $this->get_price() == 0 ) {
1015
1016
			if ( $this->is_on_sale() && $this->get_regular_price() ) {
1017
1018
				$price .= $this->get_price_html_from_to( $display_regular_price, __( 'Free!', 'woocommerce' ) );
1019
1020
				$price = apply_filters( 'woocommerce_free_sale_price_html', $price, $this );
1021
1022
			} else {
1023
1024
				$price = '<span class="amount">' . __( 'Free!', 'woocommerce' ) . '</span>';
1025
1026
				$price = apply_filters( 'woocommerce_free_price_html', $price, $this );
1027
1028
			}
1029
		}
1030
1031
		return apply_filters( 'woocommerce_get_price_html', $price, $this );
1032
	}
1033
1034
	/**
1035
	 * Functions for getting parts of a price, in html, used by get_price_html.
1036
	 *
1037
	 * @return string
1038
	 */
1039
	public function get_price_html_from_text() {
1040
		$from = '<span class="from">' . _x( 'From:', 'min_price', 'woocommerce' ) . ' </span>';
1041
1042
		return apply_filters( 'woocommerce_get_price_html_from_text', $from, $this );
1043
	}
1044
1045
	/**
1046
	 * Functions for getting parts of a price, in html, used by get_price_html.
1047
	 *
1048
	 * @param  string $from String or float to wrap with 'from' text
1049
	 * @param  mixed $to String or float to wrap with 'to' text
1050
	 * @return string
1051
	 */
1052
	public function get_price_html_from_to( $from, $to ) {
1053
		$price = '<del>' . ( ( is_numeric( $from ) ) ? wc_price( $from ) : $from ) . '</del> <ins>' . ( ( is_numeric( $to ) ) ? wc_price( $to ) : $to ) . '</ins>';
1054
1055
		return apply_filters( 'woocommerce_get_price_html_from_to', $price, $from, $to, $this );
1056
	}
1057
1058
	/**
1059
	 * Returns the tax class.
1060
	 *
1061
	 * @return string
1062
	 */
1063
	public function get_tax_class() {
1064
		return apply_filters( 'woocommerce_product_tax_class', $this->tax_class, $this );
1065
	}
1066
1067
	/**
1068
	 * Returns the tax status.
1069
	 *
1070
	 * @return string
1071
	 */
1072
	public function get_tax_status() {
1073
		return $this->tax_status;
1074
	}
1075
1076
	/**
1077
	 * Get the average rating of product. This is calculated once and stored in postmeta.
1078
	 * @return string
1079
	 */
1080
	public function get_average_rating() {
1081
		// No meta data? Do the calculation
1082
		if ( ! metadata_exists( 'post', $this->id, '_wc_average_rating' ) ) {
1083
			$this->sync_average_rating( $this->id );
1084
		}
1085
1086
		return (string) floatval( get_post_meta( $this->id, '_wc_average_rating', true ) );
1087
	}
1088
1089
	/**
1090
	 * Get the total amount (COUNT) of ratings.
1091
	 * @param  int $value Optional. Rating value to get the count for. By default returns the count of all rating values.
1092
	 * @return int
1093
	 */
1094
	public function get_rating_count( $value = null ) {
1095
		// No meta data? Do the calculation
1096
		if ( ! metadata_exists( 'post', $this->id, '_wc_rating_count' ) ) {
1097
			$this->sync_rating_count( $this->id );
1098
		}
1099
1100
		$counts = get_post_meta( $this->id, '_wc_rating_count', true );
1101
1102
		if ( is_null( $value ) ) {
1103
			return array_sum( $counts );
1104
		} else {
1105
			return isset( $counts[ $value ] ) ? $counts[ $value ] : 0;
1106
		}
1107
	}
1108
1109
	/**
1110
	 * Sync product rating. Can be called statically.
1111
	 * @param  int $post_id
1112
	 */
1113
	public static function sync_average_rating( $post_id ) {
1114
		if ( ! metadata_exists( 'post', $post_id, '_wc_rating_count' ) ) {
1115
			self::sync_rating_count( $post_id );
1116
		}
1117
1118
		$count = array_sum( (array) get_post_meta( $post_id, '_wc_rating_count', true ) );
1119
1120
		if ( $count ) {
1121
			global $wpdb;
1122
1123
			$ratings = $wpdb->get_var( $wpdb->prepare("
1124
				SELECT SUM(meta_value) FROM $wpdb->commentmeta
1125
				LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
1126
				WHERE meta_key = 'rating'
1127
				AND comment_post_ID = %d
1128
				AND comment_approved = '1'
1129
				AND meta_value > 0
1130
			", $post_id ) );
1131
			$average = number_format( $ratings / $count, 2, '.', '' );
1132
		} else {
1133
			$average = 0;
1134
		}
1135
		update_post_meta( $post_id, '_wc_average_rating', $average );
1136
	}
1137
1138
	/**
1139
	 * Sync product rating count. Can be called statically.
1140
	 * @param  int $post_id
1141
	 */
1142
	public static function sync_rating_count( $post_id ) {
1143
		global $wpdb;
1144
1145
		$counts     = array();
1146
		$raw_counts = $wpdb->get_results( $wpdb->prepare( "
1147
			SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta
1148
			LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID
1149
			WHERE meta_key = 'rating'
1150
			AND comment_post_ID = %d
1151
			AND comment_approved = '1'
1152
			AND meta_value > 0
1153
			GROUP BY meta_value
1154
		", $post_id ) );
1155
1156
		foreach ( $raw_counts as $count ) {
1157
			$counts[ $count->meta_value ] = $count->meta_value_count;
1158
		}
1159
1160
		update_post_meta( $post_id, '_wc_rating_count', $counts );
1161
	}
1162
1163
	/**
1164
	 * Returns the product rating in html format.
1165
	 *
1166
	 * @param string $rating (default: '')
1167
	 *
1168
	 * @return string
1169
	 */
1170
	public function get_rating_html( $rating = null ) {
1171
		$rating_html = '';
1172
1173
		if ( ! is_numeric( $rating ) ) {
1174
			$rating = $this->get_average_rating();
1175
		}
1176
1177
		if ( $rating > 0 ) {
1178
1179
			$rating_html  = '<div class="star-rating" title="' . sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating ) . '">';
1180
1181
			$rating_html .= '<span style="width:' . ( ( $rating / 5 ) * 100 ) . '%"><strong class="rating">' . $rating . '</strong> ' . __( 'out of 5', 'woocommerce' ) . '</span>';
1182
1183
			$rating_html .= '</div>';
1184
		}
1185
1186
		return apply_filters( 'woocommerce_product_get_rating_html', $rating_html, $rating );
1187
	}
1188
1189
	/**
1190
	 * Get the total amount (COUNT) of reviews.
1191
	 *
1192
	 * @since 2.3.2
1193
	 * @return int The total numver of product reviews
1194
	 */
1195
	public function get_review_count() {
1196
		global $wpdb;
1197
1198
		// No meta date? Do the calculation
1199
		if ( ! metadata_exists( 'post', $this->id, '_wc_review_count' ) ) {
1200
			$count = $wpdb->get_var( $wpdb->prepare("
1201
				SELECT COUNT(*) FROM $wpdb->comments
1202
				WHERE comment_parent = 0
1203
				AND comment_post_ID = %d
1204
				AND comment_approved = '1'
1205
			", $this->id ) );
1206
1207
			update_post_meta( $this->id, '_wc_review_count', $count );
1208
		} else {
1209
			$count = get_post_meta( $this->id, '_wc_review_count', true );
1210
		}
1211
1212
		return apply_filters( 'woocommerce_product_review_count', $count, $this );
1213
	}
1214
1215
	/**
1216
	 * Returns the upsell product ids.
1217
	 *
1218
	 * @return array
1219
	 */
1220
	public function get_upsells() {
1221
		return apply_filters( 'woocommerce_product_upsell_ids', (array) maybe_unserialize( $this->upsell_ids ), $this );
1222
	}
1223
1224
	/**
1225
	 * Returns the cross sell product ids.
1226
	 *
1227
	 * @return array
1228
	 */
1229
	public function get_cross_sells() {
1230
		return apply_filters( 'woocommerce_product_crosssell_ids', (array) maybe_unserialize( $this->crosssell_ids ), $this );
1231
	}
1232
1233
	/**
1234
	 * Returns the product categories.
1235
	 *
1236
	 * @param string $sep (default: ', ')
1237
	 * @param string $before (default: '')
1238
	 * @param string $after (default: '')
1239
	 * @return string
1240
	 */
1241
	public function get_categories( $sep = ', ', $before = '', $after = '' ) {
1242
		return get_the_term_list( $this->id, 'product_cat', $before, $sep, $after );
1243
	}
1244
1245
	/**
1246
	 * Returns the product tags.
1247
	 *
1248
	 * @param string $sep (default: ', ')
1249
	 * @param string $before (default: '')
1250
	 * @param string $after (default: '')
1251
	 * @return array
1252
	 */
1253
	public function get_tags( $sep = ', ', $before = '', $after = '' ) {
1254
		return get_the_term_list( $this->id, 'product_tag', $before, $sep, $after );
1255
	}
1256
1257
	/**
1258
	 * Returns the product shipping class.
1259
	 *
1260
	 * @return string
1261
	 */
1262 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...
1263
1264
		if ( ! $this->shipping_class ) {
1265
1266
			$classes = get_the_terms( $this->id, 'product_shipping_class' );
1267
1268
			if ( $classes && ! is_wp_error( $classes ) ) {
1269
				$this->shipping_class = current( $classes )->slug;
1270
			} else {
1271
				$this->shipping_class = '';
1272
			}
1273
		}
1274
1275
		return $this->shipping_class;
1276
	}
1277
1278
	/**
1279
	 * Returns the product shipping class ID.
1280
	 *
1281
	 * @return int
1282
	 */
1283 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...
1284
1285
		if ( ! $this->shipping_class_id ) {
1286
1287
			$classes = get_the_terms( $this->id, 'product_shipping_class' );
1288
1289
			if ( $classes && ! is_wp_error( $classes ) ) {
1290
				$this->shipping_class_id = current( $classes )->term_id;
1291
			} else {
1292
				$this->shipping_class_id = 0;
1293
			}
1294
		}
1295
1296
		return absint( $this->shipping_class_id );
1297
	}
1298
1299
	/**
1300
	 * Get and return related products.
1301
	 *
1302
	 * Notes:
1303
	 * 	- Results are cached in a transient for faster queries.
1304
	 *  - To make results appear random, we query and extra 10 products and shuffle them.
1305
	 *  - To ensure we always have enough results, it will check $limit before returning the cached result, if not recalc.
1306
	 *  - This used to rely on transient version to invalidate cache, but to avoid multiple transients we now just expire daily.
1307
	 *  	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.
1308
	 *  - Saving a product will flush caches for that product.
1309
	 *
1310
	 * @param int $limit (default: 5) Should be an integer greater than 0.
1311
	 * @return array Array of post IDs
1312
	 */
1313
	public function get_related( $limit = 5 ) {
1314
		global $wpdb;
1315
1316
		$transient_name = 'wc_related_' . $this->id;
1317
		$related_posts  = get_transient( $transient_name );
1318
		$limit          = $limit > 0 ? $limit : 5;
1319
1320
		// We want to query related posts if they are not cached, or we don't have enough
1321
		if ( false === $related_posts || sizeof( $related_posts ) < $limit ) {
1322
			// Related products are found from category and tag
1323
			$tags_array = $this->get_related_terms( 'product_tag' );
1324
			$cats_array = $this->get_related_terms( 'product_cat' );
1325
1326
			// Don't bother if none are set
1327
			if ( 1 === sizeof( $cats_array ) && 1 === sizeof( $tags_array ) ) {
1328
				$related_posts = array();
1329
			} else {
1330
				// Sanitize
1331
				$exclude_ids = array_map( 'absint', array_merge( array( 0, $this->id ), $this->get_upsells() ) );
1332
1333
				// Generate query - but query an extra 10 results to give the appearance of random results
1334
				$query = $this->build_related_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 );
1335
1336
				// Get the posts
1337
				$related_posts = $wpdb->get_col( implode( ' ', $query ) );
1338
			}
1339
1340
			set_transient( $transient_name, $related_posts, DAY_IN_SECONDS );
1341
		}
1342
1343
		// Randomise the results
1344
		shuffle( $related_posts );
1345
1346
		// Limit the returned results
1347
		return array_slice( $related_posts, 0, $limit );
1348
	}
1349
1350
	/**
1351
	 * Returns a single product attribute.
1352
	 *
1353
	 * @param mixed $attr
1354
	 * @return string
1355
	 */
1356
	public function get_attribute( $attr ) {
1357
1358
		$attributes = $this->get_attributes();
1359
1360
		$attr = sanitize_title( $attr );
1361
1362
		if ( isset( $attributes[ $attr ] ) || isset( $attributes[ 'pa_' . $attr ] ) ) {
1363
1364
			$attribute = isset( $attributes[ $attr ] ) ? $attributes[ $attr ] : $attributes[ 'pa_' . $attr ];
1365
1366
			if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
1367
1368
				return implode( ', ', wc_get_product_terms( $this->id, $attribute['name'], array( 'fields' => 'names' ) ) );
1369
1370
			} else {
1371
1372
				return $attribute['value'];
1373
			}
1374
		}
1375
1376
		return '';
1377
	}
1378
1379
	/**
1380
	 * Returns product attributes.
1381
	 *
1382
	 * @return array
1383
	 */
1384
	public function get_attributes() {
1385
		$attributes = array_filter( (array) maybe_unserialize( $this->product_attributes ) );
1386
		$taxonomies = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_name' );
1387
1388
		// Check for any attributes which have been removed globally
1389
		foreach ( $attributes as $key => $attribute ) {
1390
			if ( $attribute['is_taxonomy'] ) {
1391
				if ( ! in_array( substr( $attribute['name'], 3 ), $taxonomies ) ) {
1392
					unset( $attributes[ $key ] );
1393
				}
1394
			}
1395
		}
1396
1397
		return apply_filters( 'woocommerce_get_product_attributes', $attributes );
1398
	}
1399
1400
	/**
1401
	 * Returns whether or not the product has any attributes set.
1402
	 *
1403
	 * @return boolean
1404
	 */
1405
	public function has_attributes() {
1406
1407
		if ( sizeof( $this->get_attributes() ) > 0 ) {
1408
1409
			foreach ( $this->get_attributes() as $attribute ) {
1410
1411
				if ( isset( $attribute['is_visible'] ) && $attribute['is_visible'] ) {
1412
					return true;
1413
				}
1414
			}
1415
		}
1416
1417
		return false;
1418
	}
1419
1420
	/**
1421
	 * Returns whether or not we are showing dimensions on the product page.
1422
	 *
1423
	 * @return bool
1424
	 */
1425
	public function enable_dimensions_display() {
1426
		return apply_filters( 'wc_product_enable_dimensions_display', true ) && ( $this->has_dimensions() || $this->has_weight() );
1427
	}
1428
1429
	/**
1430
	 * Returns whether or not the product has dimensions set.
1431
	 *
1432
	 * @return bool
1433
	 */
1434
	public function has_dimensions() {
1435
		return $this->get_dimensions() ? true : false;
1436
	}
1437
1438
	/**
1439
	 * Does a child have dimensions set?
1440
	 * @since 2.7.0
1441
	 * @return boolean
1442
	 */
1443
	public function child_has_dimensions() {
1444
		return false;
1445
	}
1446
1447
	/**
1448
	 * Returns the product length.
1449
	 * @return string
1450
	 */
1451
	public function get_length() {
1452
		return apply_filters( 'woocommerce_product_length', $this->length ? $this->length : '', $this );
1453
	}
1454
1455
	/**
1456
	 * Returns the product width.
1457
	 * @return string
1458
	 */
1459
	public function get_width() {
1460
		return apply_filters( 'woocommerce_product_width', $this->width ? $this->width : '', $this );
1461
	}
1462
1463
	/**
1464
	 * Returns the product height.
1465
	 * @return string
1466
	 */
1467
	public function get_height() {
1468
		return apply_filters( 'woocommerce_product_height', $this->height ? $this->height : '', $this );
1469
	}
1470
1471
	/**
1472
	 * Returns the product's weight.
1473
	 * @todo   refactor filters in this class to naming woocommerce_product_METHOD
1474
	 * @return string
1475
	 */
1476
	public function get_weight() {
1477
		return apply_filters( 'woocommerce_product_weight', apply_filters( 'woocommerce_product_get_weight', $this->weight ? $this->weight : '' ), $this );
1478
	}
1479
1480
	/**
1481
	 * Returns whether or not the product has weight set.
1482
	 *
1483
	 * @return bool
1484
	 */
1485
	public function has_weight() {
1486
		return $this->get_weight() ? true : false;
1487
	}
1488
1489
	/**
1490
	 * Does a child have a weight set?
1491
	 * @since 2.7.0
1492
	 * @return boolean
1493
	 */
1494
	public function child_has_weight() {
1495
		return false;
1496
	}
1497
1498
	/**
1499
	 * Returns formatted dimensions.
1500
	 * @return string
1501
	 */
1502
	public function get_dimensions() {
1503
		$dimensions = implode( ' x ', array_filter( array(
1504
			wc_format_localized_decimal( $this->get_length() ),
1505
			wc_format_localized_decimal( $this->get_width() ),
1506
			wc_format_localized_decimal( $this->get_height() ),
1507
		) ) );
1508
1509
		if ( ! empty( $dimensions ) ) {
1510
			$dimensions .= ' ' . get_option( 'woocommerce_dimension_unit' );
1511
		}
1512
1513
		return apply_filters( 'woocommerce_product_dimensions', $dimensions, $this );
1514
	}
1515
1516
	/**
1517
	 * Lists a table of attributes for the product page.
1518
	 */
1519
	public function list_attributes() {
1520
		wc_get_template( 'single-product/product-attributes.php', array(
1521
			'product'    => $this,
1522
		) );
1523
	}
1524
1525
	/**
1526
	 * Gets the main product image ID.
1527
	 *
1528
	 * @return int
1529
	 */
1530
	public function get_image_id() {
1531
1532
		if ( has_post_thumbnail( $this->id ) ) {
1533
			$image_id = get_post_thumbnail_id( $this->id );
1534 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...
1535
			$image_id = get_post_thumbnail_id( $parent_id );
1536
		} else {
1537
			$image_id = 0;
1538
		}
1539
1540
		return $image_id;
1541
	}
1542
1543
	/**
1544
	 * Returns the main product image.
1545
	 *
1546
	 * @param string $size (default: 'shop_thumbnail')
1547
	 * @param array $attr
1548
	 * @param bool True to return $placeholder if no image is found, or false to return an empty string.
1549
	 * @return string
1550
	 */
1551
	public function get_image( $size = 'shop_thumbnail', $attr = array(), $placeholder = true ) {
1552
		if ( has_post_thumbnail( $this->id ) ) {
1553
			$image = get_the_post_thumbnail( $this->id, $size, $attr );
1554 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...
1555
			$image = get_the_post_thumbnail( $parent_id, $size, $attr );
1556
		} elseif ( $placeholder ) {
1557
			$image = wc_placeholder_img( $size );
1558
		} else {
1559
			$image = '';
1560
		}
1561
		return str_replace( array( 'https://', 'http://' ), '//', $image );
1562
	}
1563
1564
	/**
1565
	 * Get product name with SKU or ID. Used within admin.
1566
	 *
1567
	 * @return string Formatted product name
1568
	 */
1569
	public function get_formatted_name() {
1570
		if ( $this->get_sku() ) {
1571
			$identifier = $this->get_sku();
1572
		} else {
1573
			$identifier = '#' . $this->id;
1574
		}
1575
1576
		return sprintf( '%s &ndash; %s', $identifier, $this->get_title() );
1577
	}
1578
1579
	/**
1580
	 * Retrieves related product terms.
1581
	 *
1582
	 * @param string $term
1583
	 * @return array
1584
	 */
1585
	protected function get_related_terms( $term ) {
1586
		$terms_array = array( 0 );
1587
1588
		$terms = apply_filters( 'woocommerce_get_related_' . $term . '_terms', wp_get_post_terms( $this->id, $term ), $this->id );
1589
		foreach ( $terms as $term ) {
1590
			$terms_array[] = $term->term_id;
1591
		}
1592
1593
		return array_map( 'absint', $terms_array );
1594
	}
1595
1596
	/**
1597
	 * Builds the related posts query.
1598
	 *
1599
	 * @param array $cats_array
1600
	 * @param array $tags_array
1601
	 * @param array $exclude_ids
1602
	 * @param int   $limit
1603
	 * @return string
1604
	 */
1605
	protected function build_related_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
1606
		global $wpdb;
1607
1608
		$limit = absint( $limit );
1609
1610
		$query           = array();
1611
		$query['fields'] = "SELECT DISTINCT ID FROM {$wpdb->posts} p";
1612
		$query['join']   = " INNER JOIN {$wpdb->postmeta} pm ON ( pm.post_id = p.ID AND pm.meta_key='_visibility' )";
1613
		$query['join']  .= " INNER JOIN {$wpdb->term_relationships} tr ON (p.ID = tr.object_id)";
1614
		$query['join']  .= " INNER JOIN {$wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
1615
		$query['join']  .= " INNER JOIN {$wpdb->terms} t ON (t.term_id = tt.term_id)";
1616
1617
		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1618
			$query['join'] .= " INNER JOIN {$wpdb->postmeta} pm2 ON ( pm2.post_id = p.ID AND pm2.meta_key='_stock_status' )";
1619
		}
1620
1621
		$query['where']  = " WHERE 1=1";
1622
		$query['where'] .= " AND p.post_status = 'publish'";
1623
		$query['where'] .= " AND p.post_type = 'product'";
1624
		$query['where'] .= " AND p.ID NOT IN ( " . implode( ',', $exclude_ids ) . " )";
1625
		$query['where'] .= " AND pm.meta_value IN ( 'visible', 'catalog' )";
1626
1627
		if ( get_option( 'woocommerce_hide_out_of_stock_items' ) === 'yes' ) {
1628
			$query['where'] .= " AND pm2.meta_value = 'instock'";
1629
		}
1630
1631
		$relate_by_category = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $this->id );
1632
		$relate_by_tag      = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $this->id );
1633
1634
		if ( $relate_by_category || $relate_by_tag ) {
1635
			$query['where'] .= ' AND (';
1636
1637
			if ( $relate_by_category ) {
1638
				$query['where'] .= " ( tt.taxonomy = 'product_cat' AND t.term_id IN ( " . implode( ',', $cats_array ) . " ) ) ";
1639
				if ( $relate_by_tag ) {
1640
					$query['where'] .= ' OR ';
1641
				}
1642
			}
1643
1644
			if ( $relate_by_tag ) {
1645
				$query['where'] .= " ( tt.taxonomy = 'product_tag' AND t.term_id IN ( " . implode( ',', $tags_array ) . " ) ) ";
1646
			}
1647
1648
			$query['where'] .= ')';
1649
		}
1650
1651
		$query['limits'] = " LIMIT {$limit} ";
1652
		$query           = apply_filters( 'woocommerce_product_related_posts_query', $query, $this->id );
1653
1654
		return $query;
1655
	}
1656
}
1657