Completed
Pull Request — master (#11615)
by Mike
07:37
created

WC_Product::__construct()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
eloc 10
nc 4
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 ( $this->stock_status !== 'outofstock' ) {
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 ( $this->stock_status !== 'instock' ) {
249
				$this->set_stock_status( 'instock' );
250
			}
251
		}
252
	}
253
254
	/**
255
	 * Set stock level of the product.
256
	 *
257
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
258
	 * We cannot rely on the original loaded value in case another order was made since then.
259
	 *
260
	 * @param int $amount (default: null)
261
	 * @param string $mode can be set, add, or subtract
262
	 * @return int new stock level
263
	 */
264
	public function set_stock( $amount = null, $mode = 'set' ) {
265
		global $wpdb;
266
267
		if ( ! is_null( $amount ) && $this->managing_stock() ) {
268
269
			// Ensure key exists
270
			add_post_meta( $this->id, '_stock', 0, true );
271
272
			// Update stock in DB directly
273 View Code Duplication
			switch ( $mode ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
274
				case 'add' :
275
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
276
				break;
277
				case 'subtract' :
278
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
279
				break;
280
				default :
281
					$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", $amount, $this->id ) );
282
				break;
283
			}
284
285
			// Clear caches
286
			wp_cache_delete( $this->id, 'post_meta' );
287
			delete_transient( 'wc_low_stock_count' );
288
			delete_transient( 'wc_outofstock_count' );
289
			unset( $this->stock );
290
291
			// Stock status
292
			$this->check_stock_status();
293
294
			// Trigger action
295
			do_action( 'woocommerce_product_set_stock', $this );
296
		}
297
298
		return $this->get_stock_quantity();
299
	}
300
301
	/**
302
	 * Reduce stock level of the product.
303
	 *
304
	 * @param int $amount Amount to reduce by. Default: 1
305
	 * @return int new stock level
306
	 */
307
	public function reduce_stock( $amount = 1 ) {
308
		return $this->set_stock( $amount, 'subtract' );
309
	}
310
311
	/**
312
	 * Increase stock level of the product.
313
	 *
314
	 * @param int $amount Amount to increase by. Default 1.
315
	 * @return int new stock level
316
	 */
317
	public function increase_stock( $amount = 1 ) {
318
		return $this->set_stock( $amount, 'add' );
319
	}
320
321
	/**
322
	 * Set stock status of the product.
323
	 *
324
	 * @param string $status
325
	 */
326
	public function set_stock_status( $status ) {
327
328
		$status = ( 'outofstock' === $status ) ? 'outofstock' : 'instock';
329
330
		// Sanity check
331 View Code Duplication
		if ( $this->managing_stock() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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