Completed
Pull Request — master (#10592)
by Mike
09:22
created

WC_Product::get_id()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
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
	public function is_in_stock() {
613
		$status = false;
0 ignored issues
show
Unused Code introduced by
$status is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
614
		if ( $this->managing_stock() && $this->backorders_allowed() ) {
615
			$status = true;
616
		} elseif ( $this->managing_stock() && $this->get_total_stock() <= get_option( 'woocommerce_notify_no_stock_amount' ) ) {
617
			$status = false;
618
		} else {
619
			$status = $this->stock_status === 'instock';
620
		}
621
622
		return apply_filters( 'woocommerce_product_is_in_stock', $status);
623
	}
624
625
	/**
626
	 * Returns whether or not the product can be backordered.
627
	 *
628
	 * @return bool
629
	 */
630
	public function backorders_allowed() {
631
		return apply_filters( 'woocommerce_product_backorders_allowed', $this->backorders === 'yes' || $this->backorders === 'notify' ? true : false, $this->id );
632
	}
633
634
	/**
635
	 * Returns whether or not the product needs to notify the customer on backorder.
636
	 *
637
	 * @return bool
638
	 */
639
	public function backorders_require_notification() {
640
		return $this->managing_stock() && $this->backorders === 'notify' ? true : false;
641
	}
642
643
	/**
644
	 * Check if a product is on backorder.
645
	 *
646
	 * @param int $qty_in_cart (default: 0)
647
	 * @return bool
648
	 */
649
	public function is_on_backorder( $qty_in_cart = 0 ) {
650
		return $this->managing_stock() && $this->backorders_allowed() && ( $this->get_total_stock() - $qty_in_cart ) < 0 ? true : false;
651
	}
652
653
	/**
654
	 * Returns whether or not the product has enough stock for the order.
655
	 *
656
	 * @param mixed $quantity
657
	 * @return bool
658
	 */
659
	public function has_enough_stock( $quantity ) {
660
		return ! $this->managing_stock() || $this->backorders_allowed() || $this->get_stock_quantity() >= $quantity ? true : false;
661
	}
662
663
	/**
664
	 * Returns the availability of the product.
665
	 *
666
	 * @return string
667
	 */
668 View Code Duplication
	public function get_availability() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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