Completed
Push — master ( d60d3d...21ad19 )
by Mike
11:28
created

WC_Order_Item_Product::set_quantity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 2
eloc 3
c 3
b 1
f 0
nc 2
nop 1
dl 0
loc 6
rs 9.4285
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
		'id'           => 0,
24
		'name'         => '',
25
		'product_id'   => 0,
26
		'variation_id' => 0,
27
		'quantity'     => 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_quantity( 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_quantity() );
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 the Download URL.
161
	 * @param  int $download_id
162
	 * @return string
163
	 */
164
	public function get_item_download_url( $download_id ) {
165
		$order = $this->get_order();
166
167
		return $order ? add_query_arg( array(
168
			'download_file' => $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id(),
169
			'order'         => $order->get_order_key(),
170
			'email'         => urlencode( $order->get_billing_email() ),
171
			'key'           => $download_id
172
		), trailingslashit( home_url() ) ) : '';
173
	}
174
175
	/**
176
	 * Get any associated downloadable files.
177
	 * @return array
178
	 */
179
	public function get_item_downloads() {
180
		global $wpdb;
181
182
		$files   = array();
183
		$product = $this->get_product();
184
		$order   = $this->get_order();
185
186
		if ( $product && $order && $product->is_downloadable() && $order->is_download_permitted() ) {
187
			$download_ids        = $wpdb->get_col(
188
				$wpdb->prepare(
189
					"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",
190
					$order->get_billing_email(),
191
					$order->get_order_key(),
192
					$this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id()
193
				)
194
			);
195
196
			foreach ( $download_ids as $download_id ) {
197
				if ( $product->has_file( $download_id ) ) {
198
					$files[ $download_id ]                 = $product->get_file( $download_id );
199
					$files[ $download_id ]['download_url'] = $this->get_item_download_url( $download_id );
200
				}
201
			}
202
		}
203
204
		return apply_filters( 'woocommerce_get_item_downloads', $files, $this, $order );
205
	}
206
207
	/**
208
	 * Get tax status.
209
	 * @return string
210
	 */
211
	public function get_tax_status() {
212
		$product = $this->get_product();
213
		return $product ? $product->get_tax_status() : 'taxable';
214
	}
215
216
	/**
217
	 * Set meta data for backordered products.
218
	 */
219
	public function set_backorder_meta() {
220
		if ( $this->get_product()->backorders_require_notification() && $this->get_product()->is_on_backorder( $this->get_quantity() ) ) {
221
			$this->add_meta_data( apply_filters( 'woocommerce_backordered_item_meta_name', __( 'Backordered', 'woocommerce' ) ), $this->get_quantity() - max( 0, $this->get_product()->get_total_stock() ), true );
222
		}
223
	}
224
225
	/*
226
	|--------------------------------------------------------------------------
227
	| Setters
228
	|--------------------------------------------------------------------------
229
	*/
230
231
	/**
232
	 * Set quantity.
233
	 * @param int $value
234
	 */
235
	public function set_quantity( $value ) {
236
		if ( 0 >= $value ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
237
			//$this->throw_exception( __METHOD__, 'Quantity must be positive' );
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
238
		}
239
		$this->_data['quantity'] = wc_stock_amount( $value );
240
	}
241
242
	/**
243
	 * Set tax class.
244
	 * @param string $value
245
	 */
246
	public function set_tax_class( $value ) {
247
		if ( $value && ! in_array( $value, WC_Tax::get_tax_classes() ) ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
248
			//$this->throw_exception( __METHOD__, 'Invalid tax class' );
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
249
		}
250
		$this->_data['tax_class'] = $value;
251
	}
252
253
	/**
254
	 * Set Product ID
255
	 * @param int $value
256
	 */
257
	public function set_product_id( $value ) {
258
		$this->_data['product_id'] = absint( $value );
259
	}
260
261
	/**
262
	 * Set variation ID.
263
	 * @param int $value
264
	 */
265
	public function set_variation_id( $value ) {
266
		$this->_data['variation_id'] = absint( $value );
267
	}
268
269
	/**
270
	 * Line subtotal (before discounts).
271
	 * @param string $value
272
	 */
273
	public function set_subtotal( $value ) {
274
		$this->_data['subtotal'] = wc_format_decimal( $value );
275
	}
276
277
	/**
278
	 * Line total (after discounts).
279
	 * @param string $value
280
	 */
281
	public function set_total( $value ) {
282
		$this->_data['total'] = wc_format_decimal( $value );
283
	}
284
285
	/**
286
	 * Line subtotal tax (before discounts).
287
	 * @param string $value
288
	 */
289
	public function set_subtotal_tax( $value ) {
290
		$this->_data['subtotal_tax'] = wc_format_decimal( $value );
291
	}
292
293
	/**
294
	 * Line total tax (after discounts).
295
	 * @param string $value
296
	 */
297
	public function set_total_tax( $value ) {
298
		$this->_data['total_tax'] = wc_format_decimal( $value );
299
	}
300
301
	/**
302
	 * Set line taxes.
303
	 * @param array $raw_tax_data
304
	 */
305
	public function set_taxes( $raw_tax_data ) {
306
		$raw_tax_data = maybe_unserialize( $raw_tax_data );
307
		$tax_data     = array(
308
			'total'    => array(),
309
			'subtotal' => array()
310
		);
311
		if ( ! empty( $raw_tax_data['total'] ) && ! empty( $raw_tax_data['subtotal'] ) ) {
312
			$tax_data['total']    = array_map( 'wc_format_decimal', $raw_tax_data['total'] );
313
			$tax_data['subtotal'] = array_map( 'wc_format_decimal', $raw_tax_data['subtotal'] );
314
		}
315
		$this->_data['taxes'] = $tax_data;
316
	}
317
318
	/**
319
	 * Set variation data (stored as meta data - write only).
320
	 * @param array $data Key/Value pairs
321
	 */
322
	public function set_variation( $data ) {
323
		foreach ( $data as $key => $value ) {
324
			$this->add_meta_data( str_replace( 'attribute_', '', $key ), $value, true );
325
		}
326
	}
327
328
	/**
329
	 * Set properties based on passed in product object.
330
	 * @param WC_Product $product
331
	 */
332
	public function set_product( $product ) {
333
		if ( ! is_a( $product, 'WC_Product' ) ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
334
			//$this->throw_exception( __METHOD__, 'Invalid product' );
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
335
		}
336
		$this->set_product_id( $product->get_id() );
337
		$this->set_name( $product->get_title() );
338
		$this->set_tax_class( $product->get_tax_class() );
339
		$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...
340
		$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...
341
	}
342
343
	/*
344
	|--------------------------------------------------------------------------
345
	| Getters
346
	|--------------------------------------------------------------------------
347
	*/
348
349
	/**
350
	 * Get order item type.
351
	 * @return string
352
	 */
353
	public function get_type() {
354
		return 'line_item';
355
	}
356
357
	/**
358
	 * Get product ID.
359
	 * @return int
360
	 */
361
	public function get_product_id() {
362
		return absint( $this->_data['product_id'] );
363
	}
364
365
	/**
366
	 * Get variation ID.
367
	 * @return int
368
	 */
369
	public function get_variation_id() {
370
		return absint( $this->_data['variation_id'] );
371
	}
372
373
	/**
374
	 * Get quantity.
375
	 * @return int
376
	 */
377
	public function get_quantity() {
378
		return wc_stock_amount( $this->_data['quantity'] );
379
	}
380
381
	/**
382
	 * Get tax class.
383
	 * @return string
384
	 */
385
	public function get_tax_class() {
386
		return $this->_data['tax_class'];
387
	}
388
389
	/**
390
	 * Get subtotal.
391
	 * @return string
392
	 */
393
	public function get_subtotal() {
394
		return wc_format_decimal( $this->_data['subtotal'] );
395
	}
396
397
	/**
398
	 * Get subtotal tax.
399
	 * @return string
400
	 */
401
	public function get_subtotal_tax() {
402
		return wc_format_decimal( $this->_data['subtotal_tax'] );
403
	}
404
405
	/**
406
	 * Get total.
407
	 * @return string
408
	 */
409
	public function get_total() {
410
		return wc_format_decimal( $this->_data['total'] );
411
	}
412
413
	/**
414
	 * Get total tax.
415
	 * @return string
416
	 */
417
	public function get_total_tax() {
418
		return wc_format_decimal( $this->_data['total_tax'] );
419
	}
420
421
	/**
422
	 * Get fee taxes.
423
	 * @return array
424
	 */
425
	public function get_taxes() {
426
		return $this->_data['taxes'];
427
	}
428
}
429