WC_Product::is_sold_individually()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
eloc 5
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
		return $this->set_stock( $amount, 'subtract' );
311
	}
312
313
	/**
314
	 * Increase stock level of the product.
315
	 *
316
	 * @param int $amount Amount to increase by. Default 1.
317
	 * @return int new stock level
318
	 */
319
	public function increase_stock( $amount = 1 ) {
320
		return $this->set_stock( $amount, 'add' );
321
	}
322
323
	/**
324
	 * Set stock status of the product.
325
	 *
326
	 * @param string $status
327
	 */
328
	public function set_stock_status( $status ) {
329
330
		$status = ( 'outofstock' === $status ) ? 'outofstock' : 'instock';
331
332
		// Sanity check
333 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...
334
			if ( ! $this->backorders_allowed() && $this->get_stock_quantity() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
335
				$status = 'outofstock';
336
			}
337
		}
338
339
		if ( update_post_meta( $this->id, '_stock_status', $status ) ) {
340
			$this->stock_status = $status;
341
			do_action( 'woocommerce_product_set_stock_status', $this->id, $status );
342
		}
343
	}
344
345
	/**
346
	 * Return the product type.
347
	 *
348
	 * @return string
349
	 */
350
	public function get_type() {
351
		return is_null( $this->product_type ) ? '' : $this->product_type;
352
	}
353
354
	/**
355
	 * Checks the product type.
356
	 *
357
	 * Backwards compat with downloadable/virtual.
358
	 *
359
	 * @param string $type Array or string of types
360
	 * @return bool
361
	 */
362
	public function is_type( $type ) {
363
		return ( $this->product_type == $type || ( is_array( $type ) && in_array( $this->product_type, $type ) ) ) ? true : false;
364
	}
365
366
	/**
367
	 * Checks if a product is downloadable.
368
	 *
369
	 * @return bool
370
	 */
371
	public function is_downloadable() {
372
		return $this->downloadable == 'yes' ? true : false;
373
	}
374
375
	/**
376
	 * Check if downloadable product has a file attached.
377
	 *
378
	 * @since 1.6.2
379
	 *
380
	 * @param string $download_id file identifier
381
	 * @return bool Whether downloadable product has a file attached.
382
	 */
383
	public function has_file( $download_id = '' ) {
384
		return ( $this->is_downloadable() && $this->get_file( $download_id ) ) ? true : false;
385
	}
386
387
	/**
388
	 * Gets an array of downloadable files for this product.
389
	 *
390
	 * @since 2.1.0
391
	 *
392
	 * @return array
393
	 */
394
	public function get_files() {
395
396
		$downloadable_files = array_filter( isset( $this->downloadable_files ) ? (array) maybe_unserialize( $this->downloadable_files ) : array() );
397
398
		if ( ! empty( $downloadable_files ) ) {
399
400
			foreach ( $downloadable_files as $key => $file ) {
401
402
				if ( ! is_array( $file ) ) {
403
					$downloadable_files[ $key ] = array(
404
						'file' => $file,
405
						'name' => ''
406
					);
407
				}
408
409
				// Set default name
410
				if ( empty( $file['name'] ) ) {
411
					$downloadable_files[ $key ]['name'] = wc_get_filename_from_url( $file['file'] );
412
				}
413
414
				// Filter URL
415
				$downloadable_files[ $key ]['file'] = apply_filters( 'woocommerce_file_download_path', $downloadable_files[ $key ]['file'], $this, $key );
416
			}
417
		}
418
419
		return apply_filters( 'woocommerce_product_files', $downloadable_files, $this );
420
	}
421
422
	/**
423
	 * Get a file by $download_id.
424
	 *
425
	 * @param string $download_id file identifier
426
	 * @return array|false if not found
427
	 */
428
	public function get_file( $download_id = '' ) {
429
430
		$files = $this->get_files();
431
432
		if ( '' === $download_id ) {
433
			$file = sizeof( $files ) ? current( $files ) : false;
434
		} elseif ( isset( $files[ $download_id ] ) ) {
435
			$file = $files[ $download_id ];
436
		} else {
437
			$file = false;
438
		}
439
440
		// allow overriding based on the particular file being requested
441
		return apply_filters( 'woocommerce_product_file', $file, $this, $download_id );
442
	}
443
444
	/**
445
	 * Get file download path identified by $download_id.
446
	 *
447
	 * @param string $download_id file identifier
448
	 * @return string
449
	 */
450
	public function get_file_download_path( $download_id ) {
451
		$files = $this->get_files();
452
453
		if ( isset( $files[ $download_id ] ) ) {
454
			$file_path = $files[ $download_id ]['file'];
455
		} else {
456
			$file_path = '';
457
		}
458
459
		// allow overriding based on the particular file being requested
460
		return apply_filters( 'woocommerce_product_file_download_path', $file_path, $this, $download_id );
461
	}
462
463
	/**
464
	 * Checks if a product is virtual (has no shipping).
465
	 *
466
	 * @return bool
467
	 */
468
	public function is_virtual() {
469
		return apply_filters( 'woocommerce_is_virtual', $this->virtual == 'yes' ? true : false, $this );
470
	}
471
472
	/**
473
	 * Checks if a product needs shipping.
474
	 *
475
	 * @return bool
476
	 */
477
	public function needs_shipping() {
478
		return apply_filters( 'woocommerce_product_needs_shipping', $this->is_virtual() ? false : true, $this );
479
	}
480
481
	/**
482
	 * Check if a product is sold individually (no quantities).
483
	 *
484
	 * @return bool
485
	 */
486
	public function is_sold_individually() {
487
488
		$return = false;
489
490
		if ( 'yes' == $this->sold_individually ) {
491
			$return = true;
492
		}
493
494
		return apply_filters( 'woocommerce_is_sold_individually', $return, $this );
495
	}
496
497
	/**
498
	 * Returns the child product.
499
	 *
500
	 * @param mixed $child_id
501
	 * @return WC_Product|WC_Product|WC_Product_variation
502
	 */
503
	public function get_child( $child_id ) {
504
		return wc_get_product( $child_id );
505
	}
506
507
	/**
508
	 * Returns the children.
509
	 *
510
	 * @return array
511
	 */
512
	public function get_children() {
513
		return array();
514
	}
515
516
	/**
517
	 * Returns whether or not the product has any child product.
518
	 *
519
	 * @return bool
520
	 */
521
	public function has_child() {
522
		return false;
523
	}
524
525
	/**
526
	 * Returns whether or not the product post exists.
527
	 *
528
	 * @return bool
529
	 */
530
	public function exists() {
531
		return empty( $this->post ) ? false : true;
532
	}
533
534
	/**
535
	 * Returns whether or not the product is taxable.
536
	 *
537
	 * @return bool
538
	 */
539
	public function is_taxable() {
540
		$taxable = $this->get_tax_status() === 'taxable' && wc_tax_enabled() ? true : false;
541
		return apply_filters( 'woocommerce_product_is_taxable', $taxable, $this );
542
	}
543
544
	/**
545
	 * Returns whether or not the product shipping is taxable.
546
	 *
547
	 * @return bool
548
	 */
549
	public function is_shipping_taxable() {
550
		return $this->get_tax_status() === 'taxable' || $this->get_tax_status() === 'shipping' ? true : false;
551
	}
552
553
	/**
554
	 * Get the title of the post.
555
	 *
556
	 * @return string
557
	 */
558
	public function get_title() {
559
		return apply_filters( 'woocommerce_product_title', $this->post ? $this->post->post_title : '', $this );
560
	}
561
562
	/**
563
	 * Get the parent of the post.
564
	 *
565
	 * @return int
566
	 */
567
	public function get_parent() {
568
		return apply_filters( 'woocommerce_product_parent', absint( $this->post->post_parent ), $this );
569
	}
570
571
	/**
572
	 * Get the add to url used mainly in loops.
573
	 *
574
	 * @return string
575
	 */
576
	public function add_to_cart_url() {
577
		return apply_filters( 'woocommerce_product_add_to_cart_url', get_permalink( $this->id ), $this );
578
	}
579
580
	/**
581
	 * Get the add to cart button text for the single page.
582
	 *
583
	 * @return string
584
	 */
585
	public function single_add_to_cart_text() {
586
		return apply_filters( 'woocommerce_product_single_add_to_cart_text', __( 'Add to cart', 'woocommerce' ), $this );
587
	}
588
589
	/**
590
	 * Get the add to cart button text.
591
	 *
592
	 * @return string
593
	 */
594
	public function add_to_cart_text() {
595
		return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'Read more', 'woocommerce' ), $this );
596
	}
597
598
	/**
599
	 * Returns whether or not the product is stock managed.
600
	 *
601
	 * @return bool
602
	 */
603
	public function managing_stock() {
604
		return ( ! isset( $this->manage_stock ) || $this->manage_stock == 'no' || get_option( 'woocommerce_manage_stock' ) !== 'yes' ) ? false : true;
605
	}
606
607
	/**
608
	 * Returns whether or not the product is in stock.
609
	 *
610
	 * @return bool
611
	 */
612 View Code Duplication
	public function is_in_stock() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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