Completed
Pull Request — master (#11208)
by Mike
09:06
created

WC_Order_Item_Product   C

Complexity

Total Complexity 59

Size/Duplication

Total Lines 389
Duplicated Lines 7.2 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 28
loc 389
rs 6.1904
wmc 59
lcom 1
cbo 2

30 Methods

Rating   Name   Duplication   Size   Complexity  
B offsetGet() 14 14 6
B offsetSet() 14 14 6
A offsetExists() 0 6 2
A read() 0 14 2
A save() 0 16 2
A get_internal_meta_keys() 0 3 1
A get_product() 0 14 3
C get_item_downloads() 0 33 8
A get_tax_status() 0 4 2
A set_qty() 0 3 1
A set_tax_class() 0 3 1
A set_product_id() 0 3 1
A set_variation_id() 0 3 1
A set_subtotal() 0 3 1
A set_total() 0 3 1
A set_subtotal_tax() 0 3 1
A set_total_tax() 0 3 1
A set_taxes() 0 12 3
A set_variation() 0 5 2
A set_product() 0 9 4
A get_type() 0 3 1
A get_product_id() 0 3 1
A get_variation_id() 0 3 1
A get_qty() 0 3 1
A get_tax_class() 0 3 1
A get_subtotal() 0 3 1
A get_subtotal_tax() 0 3 1
A get_total() 0 3 1
A get_total_tax() 0 3 1
A get_taxes() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Order_Item_Product often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Order_Item_Product, and based on these observations, apply Extract Interface, too.

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Order Line Item (product).
8
 *
9
 * @version     2.7.0
10
 * @since       2.7.0
11
 * @package     WooCommerce/Classes
12
 * @author      WooThemes
13
 */
14
class WC_Order_Item_Product extends WC_Order_Item {
15
16
	/**
17
	 * Data properties of this order item object.
18
	 * @since 2.7.0
19
	 * @var array
20
	 */
21
	protected $_data = array(
22
		'order_id'      => 0,
23
		'order_item_id' => 0,
24
		'name'          => '',
25
		'product_id'    => 0,
26
		'variation_id'  => 0,
27
		'qty'           => 0,
28
		'tax_class'     => '',
29
		'subtotal'      => 0,
30
		'subtotal_tax'  => 0,
31
		'total'         => 0,
32
		'total_tax'     => 0,
33
		'taxes'         => array(
34
			'subtotal' => array(),
35
			'total'    => array()
36
		),
37
	);
38
39
	/**
40
	 * offsetGet for ArrayAccess/Backwards compatibility.
41
	 * @deprecated Add deprecation notices in future release.
42
	 * @param string $offset
43
	 * @return mixed
44
	 */
45 View Code Duplication
	public function offsetGet( $offset ) {
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...
46
		if ( 'line_subtotal' === $offset ) {
47
			$offset = 'subtotal';
48
		} elseif ( 'line_subtotal_tax' === $offset ) {
49
			$offset = 'subtotal_tax';
50
		} elseif ( 'line_total' === $offset ) {
51
			$offset = 'total';
52
		} elseif ( 'line_tax' === $offset ) {
53
			$offset = 'total_tax';
54
		} elseif ( 'line_tax_data' === $offset ) {
55
			$offset = 'taxes';
56
		}
57
		return parent::offsetGet( $offset );
58
	}
59
60
	/**
61
	 * offsetSet for ArrayAccess/Backwards compatibility.
62
	 * @deprecated Add deprecation notices in future release.
63
	 * @param string $offset
64
	 * @param mixed $value
65
	 */
66 View Code Duplication
	public function offsetSet( $offset, $value ) {
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...
67
		if ( 'line_subtotal' === $offset ) {
68
			$offset = 'subtotal';
69
		} elseif ( 'line_subtotal_tax' === $offset ) {
70
			$offset = 'subtotal_tax';
71
		} elseif ( 'line_total' === $offset ) {
72
			$offset = 'total';
73
		} elseif ( 'line_tax' === $offset ) {
74
			$offset = 'total_tax';
75
		} elseif ( 'line_tax_data' === $offset ) {
76
			$offset = 'taxes';
77
		}
78
		parent::offsetSet( $offset, $value );
79
	}
80
81
	/**
82
	 * offsetExists for ArrayAccess
83
	 * @param string $offset
84
	 * @return bool
85
	 */
86
	public function offsetExists( $offset ) {
87
		if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta' ) ) ) {
88
			return true;
89
		}
90
		return parent::offsetExists( $offset );
91
	}
92
93
	/**
94
	 * Read/populate data properties specific to this order item.
95
	 */
96
	public function read( $id ) {
97
		parent::read( $id );
98
		if ( $this->get_id() ) {
99
			$this->set_product_id( get_metadata( 'order_item', $this->get_id(), '_product_id', true ) );
100
			$this->set_variation_id( get_metadata( 'order_item', $this->get_id(), '_variation_id', true ) );
101
			$this->set_qty( get_metadata( 'order_item', $this->get_id(), '_qty', true ) );
102
			$this->set_tax_class( get_metadata( 'order_item', $this->get_id(), '_tax_class', true ) );
103
			$this->set_subtotal( get_metadata( 'order_item', $this->get_id(), '_line_subtotal', true ) );
104
			$this->set_subtotal_tax( get_metadata( 'order_item', $this->get_id(), '_line_subtotal_tax', true ) );
105
			$this->set_total( get_metadata( 'order_item', $this->get_id(), '_line_total', true ) );
106
			$this->set_total_tax( get_metadata( 'order_item', $this->get_id(), '_line_tax', true ) );
107
			$this->set_taxes( get_metadata( 'order_item', $this->get_id(), '_line_tax_data', true ) );
108
		}
109
	}
110
111
	/**
112
	 * Save properties specific to this order item.
113
	 * @return int Item ID
114
	 */
115
	public function save() {
116
		parent::save();
117
		if ( $this->get_id() ) {
118
			wc_update_order_item_meta( $this->get_id(), '_product_id', $this->get_product_id() );
119
			wc_update_order_item_meta( $this->get_id(), '_variation_id', $this->get_variation_id() );
120
			wc_update_order_item_meta( $this->get_id(), '_qty', $this->get_qty() );
121
			wc_update_order_item_meta( $this->get_id(), '_tax_class', $this->get_tax_class() );
122
			wc_update_order_item_meta( $this->get_id(), '_line_subtotal', $this->get_subtotal() );
123
			wc_update_order_item_meta( $this->get_id(), '_line_subtotal_tax', $this->get_subtotal_tax() );
124
			wc_update_order_item_meta( $this->get_id(), '_line_total', $this->get_total() );
125
			wc_update_order_item_meta( $this->get_id(), '_line_tax', $this->get_total_tax() );
126
			wc_update_order_item_meta( $this->get_id(), '_line_tax_data', $this->get_taxes() );
127
		}
128
129
		return $this->get_id();
130
	}
131
132
	/**
133
	 * Internal meta keys we don't want exposed as part of meta_data.
134
	 * @return array()
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
135
	 */
136
	protected function get_internal_meta_keys() {
137
		return array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );
138
	}
139
140
	/**
141
	 * Get the associated product.
142
	 * @return WC_Product|bool
143
	 */
144
	public function get_product() {
145
		if ( $this->get_variation_id() ) {
146
			$product = wc_get_product( $this->get_variation_id() );
147
		} else {
148
			$product = wc_get_product( $this->get_product_id() );
149
		}
150
151
		// Backwards compatible filter from WC_Order::get_product_from_item()
152
		if ( has_filter( 'woocommerce_get_product_from_item' ) ) {
153
			$product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, wc_get_order( $this->get_order_id() ) );
154
		}
155
156
		return apply_filters( 'woocommerce_order_item_product', $product, $this );
157
	}
158
159
	/**
160
	 * Get any associated downloadable files.
161
	 * @return array
162
	 */
163
	public function get_item_downloads() {
164
		global $wpdb;
165
166
		$files   = array();
167
		$product = $this->get_product();
168
		$order   = wc_get_order( $this->get_order_id() );
169
170
		if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) {
171
			$download_file_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id();
172
			$download_ids     = $wpdb->get_col(
173
				$wpdb->prepare(
174
					"SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id",
175
					$order->get_billing_email(),
176
					$order->get_order_key(),
177
					$download_file_id
178
				)
179
			);
180
181
			foreach ( $download_ids as $download_id ) {
182
				if ( $product->has_file( $download_id ) ) {
183
					$files[ $download_id ] = $product->get_file( $download_id );
184
					$files[ $download_id ]['download_url'] = add_query_arg( array(
185
						'download_file' => $download_file_id,
186
						'order'         => $order->get_order_key(),
187
						'email'         => urlencode( $order->get_billing_email() ),
188
						'key'           => $download_id
189
					), trailingslashit( home_url() ) );
190
				}
191
			}
192
		}
193
194
		return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order );
195
	}
196
197
	/**
198
	 * Get tax status.
199
	 * @return string
200
	 */
201
	public function get_tax_status() {
202
		$product = $this->get_product();
203
		return $product ? $product->get_tax_status() : 'taxable';
204
	}
205
206
	/*
207
	|--------------------------------------------------------------------------
208
	| Setters
209
	|--------------------------------------------------------------------------
210
	*/
211
212
	/**
213
	 * Set qty.
214
	 * @param int $value
215
	 */
216
	public function set_qty( $value ) {
217
		$this->_data['qty'] = wc_stock_amount( $value );
218
	}
219
220
	/**
221
	 * Set tax class.
222
	 * @param string $value
223
	 */
224
	public function set_tax_class( $value ) {
225
		$this->_data['tax_class'] = $value;
226
	}
227
228
	/**
229
	 * Set Product ID
230
	 * @param int $value
231
	 */
232
	public function set_product_id( $value ) {
233
		$this->_data['product_id'] = absint( $value );
234
	}
235
236
	/**
237
	 * Set variation ID.
238
	 * @param int $value
239
	 */
240
	public function set_variation_id( $value ) {
241
		$this->_data['variation_id'] = absint( $value );
242
	}
243
244
	/**
245
	 * Line subtotal (before discounts).
246
	 * @param string $value
247
	 */
248
	public function set_subtotal( $value ) {
249
		$this->_data['subtotal'] = wc_format_decimal( $value );
250
	}
251
252
	/**
253
	 * Line total (after discounts).
254
	 * @param string $value
255
	 */
256
	public function set_total( $value ) {
257
		$this->_data['total'] = wc_format_decimal( $value );
258
	}
259
260
	/**
261
	 * Line subtotal tax (before discounts).
262
	 * @param string $value
263
	 */
264
	public function set_subtotal_tax( $value ) {
265
		$this->_data['subtotal_tax'] = wc_format_decimal( $value );
266
	}
267
268
	/**
269
	 * Line total tax (after discounts).
270
	 * @param string $value
271
	 */
272
	public function set_total_tax( $value ) {
273
		$this->_data['total_tax'] = wc_format_decimal( $value );
274
	}
275
276
	/**
277
	 * Set line taxes.
278
	 * @param array $raw_tax_data
279
	 */
280
	public function set_taxes( $raw_tax_data ) {
281
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
282
		$tax_data     = array(
283
			'total'    => array(),
284
			'subtotal' => array()
285
		);
286
		if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) {
287
			$tax_data['total']    = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
288
			$tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] );
289
		}
290
		$this->_data['taxes'] = $tax_data;
291
	}
292
293
	/**
294
	 * Set variation data (stored as meta data - write only).
295
	 * @param array $data Key/Value pairs
296
	 */
297
	public function set_variation( $data ) {
298
		foreach ( $data as $key => $value ) {
299
			$this->_meta_data[ str_replace( 'attribute_', '', $key ) ] = $value;
300
		}
301
	}
302
303
	/**
304
	 * Set properties based on passed in product object.
305
	 * @param WC_Product $product
306
	 */
307
	public function set_product( $product ) {
308
		if ( $product ) {
309
			$this->set_product_id( $product->get_id() );
310
			$this->set_name( $product->get_title() );
311
			$this->set_tax_class( $product->get_tax_class() );
312
			$this->set_variation_id( is_callable( array( $product, 'get_variation_id' ) ) ? $product->get_variation_id() : 0 );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WC_Product as the method get_variation_id() does only exist in the following sub-classes of WC_Product: WC_Product_Variation. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
313
			$this->set_variation( is_callable( array( $product, 'get_variation_attributes' ) ) ? $product->get_variation_attributes() : array() );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class WC_Product as the method get_variation_attributes() does only exist in the following sub-classes of WC_Product: WC_Product_Variable, WC_Product_Variation. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
314
		}
315
	}
316
317
	/*
318
	|--------------------------------------------------------------------------
319
	| Getters
320
	|--------------------------------------------------------------------------
321
	*/
322
323
	/**
324
	 * Get order item type.
325
	 * @return string
326
	 */
327
	public function get_type() {
328
		return 'line_item';
329
	}
330
331
	/**
332
	 * Get product ID.
333
	 * @return int
334
	 */
335
	public function get_product_id() {
336
		return absint( $this->_data['product_id'] );
337
	}
338
339
	/**
340
	 * Get variation ID.
341
	 * @return int
342
	 */
343
	public function get_variation_id() {
344
		return absint( $this->_data['variation_id'] );
345
	}
346
347
	/**
348
	 * Get qty.
349
	 * @return int
350
	 */
351
	public function get_qty() {
352
		return wc_stock_amount( $this->_data['qty'] );
353
	}
354
355
	/**
356
	 * Get tax class.
357
	 * @return string
358
	 */
359
	public function get_tax_class() {
360
		return $this->_data['tax_class'];
361
	}
362
363
	/**
364
	 * Get subtotal.
365
	 * @return string
366
	 */
367
	public function get_subtotal() {
368
		return wc_format_decimal( $this->_data['subtotal'] );
369
	}
370
371
	/**
372
	 * Get subtotal tax.
373
	 * @return string
374
	 */
375
	public function get_subtotal_tax() {
376
		return wc_format_decimal( $this->_data['subtotal_tax'] );
377
	}
378
379
	/**
380
	 * Get total.
381
	 * @return string
382
	 */
383
	public function get_total() {
384
		return wc_format_decimal( $this->_data['total'] );
385
	}
386
387
	/**
388
	 * Get total tax.
389
	 * @return string
390
	 */
391
	public function get_total_tax() {
392
		return wc_format_decimal( $this->_data['total_tax'] );
393
	}
394
395
	/**
396
	 * Get fee taxes.
397
	 * @return array
398
	 */
399
	public function get_taxes() {
400
		return $this->_data['taxes'];
401
	}
402
}
403