Completed
Pull Request — master (#11858)
by
unknown
13:04
created

WC_Product::__isset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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