Completed
Pull Request — master (#10259)
by Mike
08:16
created

WC_Order_Item_Product   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 53
c 7
b 0
f 0
lcom 1
cbo 3
dl 0
loc 368
rs 7.4757

29 Methods

Rating   Name   Duplication   Size   Complexity  
B offsetGet() 0 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   Complexity   

Complex Class

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
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 14 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Order Line Item (product).
8
 *
9
 * @version     2.6.0
10
 * @since       2.6.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.6.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
	public function offsetGet( $offset ) {
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
	 * offsetExists for ArrayAccess
62
	 * @param string $offset
63
	 * @return bool
64
	 */
65
	public function offsetExists( $offset ) {
66
		if ( in_array( $offset, array( 'line_subtotal', 'line_subtotal_tax', 'line_total', 'line_tax', 'line_tax_data', 'item_meta_array', 'item_meta' ) ) ) {
67
			return true;
68
		}
69
		return parent::offsetExists( $offset );
70
	}
71
72
	/**
73
	 * Read/populate data properties specific to this order item.
74
	 */
75
	public function read( $id ) {
76
		parent::read( $id );
77
		if ( $this->get_id() ) {
78
			$this->set_product_id( get_metadata( 'order_item', $this->get_id(), '_product_id', true ) );
79
			$this->set_variation_id( get_metadata( 'order_item', $this->get_id(), '_variation_id', true ) );
80
			$this->set_qty( get_metadata( 'order_item', $this->get_id(), '_qty', true ) );
81
			$this->set_tax_class( get_metadata( 'order_item', $this->get_id(), '_tax_class', true ) );
82
			$this->set_subtotal( get_metadata( 'order_item', $this->get_id(), '_line_subtotal', true ) );
83
			$this->set_subtotal_tax( get_metadata( 'order_item', $this->get_id(), '_line_subtotal_tax', true ) );
84
			$this->set_total( get_metadata( 'order_item', $this->get_id(), '_line_total', true ) );
85
			$this->set_total_tax( get_metadata( 'order_item', $this->get_id(), '_line_tax', true ) );
86
			$this->set_taxes( get_metadata( 'order_item', $this->get_id(), '_line_tax_data', true ) );
87
		}
88
	}
89
90
	/**
91
	 * Save properties specific to this order item.
92
	 * @return int Item ID
93
	 */
94
	public function save() {
95
		parent::save();
96
		if ( $this->get_id() ) {
97
			wc_update_order_item_meta( $this->get_id(), '_product_id', $this->get_product_id() );
98
			wc_update_order_item_meta( $this->get_id(), '_variation_id', $this->get_variation_id() );
99
			wc_update_order_item_meta( $this->get_id(), '_qty', $this->get_qty() );
100
			wc_update_order_item_meta( $this->get_id(), '_tax_class', $this->get_tax_class() );
101
			wc_update_order_item_meta( $this->get_id(), '_line_subtotal', $this->get_subtotal() );
102
			wc_update_order_item_meta( $this->get_id(), '_line_subtotal_tax', $this->get_subtotal_tax() );
103
			wc_update_order_item_meta( $this->get_id(), '_line_total', $this->get_total() );
104
			wc_update_order_item_meta( $this->get_id(), '_line_tax', $this->get_total_tax() );
105
			wc_update_order_item_meta( $this->get_id(), '_line_tax_data', $this->get_taxes() );
106
		}
107
108
		return $this->get_id();
109
	}
110
111
	/**
112
	 * Internal meta keys we don't want exposed as part of meta_data.
113
	 * @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...
114
	 */
115
	protected function get_internal_meta_keys() {
116
		return array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );
117
	}
118
119
	/**
120
	 * Get the associated product.
121
	 * @return WC_Product|bool
122
	 */
123
	public function get_product() {
124
		if ( $this->get_variation_id() ) {
125
			$product = wc_get_product( $this->get_variation_id() );
126
		} else {
127
			$product = wc_get_product( $this->get_product_id() );
128
		}
129
130
		// Backwards compatible filter from WC_Order::get_product_from_item()
131
		if ( has_filter( 'woocommerce_get_product_from_item' ) ) {
132
			$product = apply_filters( 'woocommerce_get_product_from_item', $product, $this, wc_get_order( $this->get_order_id() ) );
133
		}
134
135
		return apply_filters( 'woocommerce_order_item_product', $product, $this );
136
	}
137
138
	/**
139
	 * Get any associated downloadable files.
140
	 * @return array
141
	 */
142
	public function get_item_downloads() {
143
		global $wpdb;
144
145
		$files   = array();
146
		$product = $this->get_product();
147
		$order   = wc_get_order( $this->get_order_id() );
148
149
		if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) {
150
			$download_file_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id();
151
			$download_ids     = $wpdb->get_col(
152
				$wpdb->prepare(
153
					"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",
154
					$order->get_billing_email(),
155
					$order->get_order_key(),
156
					$download_file_id
157
				)
158
			);
159
160
			foreach ( $download_ids as $download_id ) {
161
				if ( $product->has_file( $download_id ) ) {
162
					$files[ $download_id ] = $product->get_file( $download_id );
163
					$files[ $download_id ]['download_url'] = add_query_arg( array(
164
						'download_file' => $download_file_id,
165
						'order'         => $order->get_order_key(),
166
						'email'         => urlencode( $order->get_billing_email() ),
167
						'key'           => $download_id
168
					), trailingslashit( home_url() ) );
169
				}
170
			}
171
		}
172
173
		return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order );
174
	}
175
176
	/**
177
	 * Get tax status.
178
	 * @return string
179
	 */
180
	public function get_tax_status() {
181
		$product = $this->get_product();
182
		return $product ? $product->get_tax_status() : 'taxable';
183
	}
184
185
	/*
186
	|--------------------------------------------------------------------------
187
	| Setters
188
	|--------------------------------------------------------------------------
189
	*/
190
191
	/**
192
	 * Set qty.
193
	 * @param int $value
194
	 */
195
	public function set_qty( $value ) {
196
		$this->_data['qty'] = wc_stock_amount( $value );
197
	}
198
199
	/**
200
	 * Set tax class.
201
	 * @param string $value
202
	 */
203
	public function set_tax_class( $value ) {
204
		$this->_data['tax_class'] = $value;
205
	}
206
207
	/**
208
	 * Set Product ID
209
	 * @param int $value
210
	 */
211
	public function set_product_id( $value ) {
212
		$this->_data['product_id'] = absint( $value );
213
	}
214
215
	/**
216
	 * Set variation ID.
217
	 * @param int $value
218
	 */
219
	public function set_variation_id( $value ) {
220
		$this->_data['variation_id'] = absint( $value );
221
	}
222
223
	/**
224
	 * Line subtotal (before discounts).
225
	 * @param string $value
226
	 */
227
	public function set_subtotal( $value ) {
228
		$this->_data['subtotal'] = wc_format_decimal( $value );
229
	}
230
231
	/**
232
	 * Line total (after discounts).
233
	 * @param string $value
234
	 */
235
	public function set_total( $value ) {
236
		$this->_data['total'] = wc_format_decimal( $value );
237
	}
238
239
	/**
240
	 * Line subtotal tax (before discounts).
241
	 * @param string $value
242
	 */
243
	public function set_subtotal_tax( $value ) {
244
		$this->_data['subtotal_tax'] = wc_format_decimal( $value );
245
	}
246
247
	/**
248
	 * Line total tax (after discounts).
249
	 * @param string $value
250
	 */
251
	public function set_total_tax( $value ) {
252
		$this->_data['total_tax'] = wc_format_decimal( $value );
253
	}
254
255
	/**
256
	 * Set line taxes.
257
	 * @param array $raw_tax_data
258
	 */
259
	public function set_taxes( $raw_tax_data ) {
260
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
261
		$tax_data     = array(
262
			'total'    => array(),
263
			'subtotal' => array()
264
		);
265
		if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) {
266
			$tax_data['total']    = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
267
			$tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] );
268
		}
269
		$this->_data['taxes'] = $tax_data;
270
	}
271
272
	/**
273
	 * Set variation data (stored as meta data - write only).
274
	 * @param array $data Key/Value pairs
275
	 */
276
	public function set_variation( $data ) {
277
		foreach ( $data as $key => $value ) {
278
			$this->_meta_data[ str_replace( 'attribute_', '', $key ) ] = $value;
279
		}
280
	}
281
282
	/**
283
	 * Set properties based on passed in product object.
284
	 * @param WC_Product $product
285
	 */
286
	public function set_product( $product ) {
287
		if ( $product ) {
288
			$this->set_product_id( $product->get_id() );
289
			$this->set_name( $product->get_title() );
290
			$this->set_tax_class( $product->get_tax_class() );
291
			$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...
292
			$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...
293
		}
294
	}
295
296
	/*
297
	|--------------------------------------------------------------------------
298
	| Getters
299
	|--------------------------------------------------------------------------
300
	*/
301
302
	/**
303
	 * Get order item type.
304
	 * @return string
305
	 */
306
	public function get_type() {
307
		return 'line_item';
308
	}
309
310
	/**
311
	 * Get product ID.
312
	 * @return int
313
	 */
314
	public function get_product_id() {
315
		return absint( $this->_data['product_id'] );
316
	}
317
318
	/**
319
	 * Get variation ID.
320
	 * @return int
321
	 */
322
	public function get_variation_id() {
323
		return absint( $this->_data['variation_id'] );
324
	}
325
326
	/**
327
	 * Get qty.
328
	 * @return int
329
	 */
330
	public function get_qty() {
331
		return wc_stock_amount( $this->_data['qty'] );
332
	}
333
334
	/**
335
	 * Get tax class.
336
	 * @return string
337
	 */
338
	public function get_tax_class() {
339
		return $this->_data['tax_class'];
340
	}
341
342
	/**
343
	 * Get subtotal.
344
	 * @return string
345
	 */
346
	public function get_subtotal() {
347
		return wc_format_decimal( $this->_data['subtotal'] );
348
	}
349
350
	/**
351
	 * Get subtotal tax.
352
	 * @return string
353
	 */
354
	public function get_subtotal_tax() {
355
		return wc_format_decimal( $this->_data['subtotal_tax'] );
356
	}
357
358
	/**
359
	 * Get total.
360
	 * @return string
361
	 */
362
	public function get_total() {
363
		return wc_format_decimal( $this->_data['total'] );
364
	}
365
366
	/**
367
	 * Get total tax.
368
	 * @return string
369
	 */
370
	public function get_total_tax() {
371
		return wc_format_decimal( $this->_data['total_tax'] );
372
	}
373
374
	/**
375
	 * Get fee taxes.
376
	 * @return array
377
	 */
378
	public function get_taxes() {
379
		return $this->_data['taxes'];
380
	}
381
}
382