Completed
Push — master ( 34767e...fa30a3 )
by Mike
07:16
created

WC_Shipping::calculate_shipping()   D

Complexity

Conditions 19
Paths 125

Size

Total Lines 78
Code Lines 34

Duplication

Lines 3
Ratio 3.85 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 3
loc 78
rs 4.7752
cc 19
eloc 34
nc 125
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 21 and the first side effect is on line 15.

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
/**
3
 * WooCommerce Shipping Class
4
 *
5
 * Handles shipping and loads shipping methods via hooks.
6
 *
7
 * @class 		WC_Shipping
8
 * @version		2.6.0
9
 * @package		WooCommerce/Classes/Shipping
10
 * @category	Class
11
 * @author 		WooThemes
12
 */
13
14
if ( ! defined( 'ABSPATH' ) ) {
15
	exit;
16
}
17
18
/**
19
 * WC_Shipping
20
 */
21
class WC_Shipping {
22
23
	/** @var bool True if shipping is enabled. */
24
	public $enabled					 = false;
25
26
	/** @var array Stores methods loaded into woocommerce. */
27
	public $shipping_methods         = null;
28
29
	/** @var float Stores the cost of shipping */
30
	public $shipping_total 			 = 0;
31
32
	/** @var array Stores an array of shipping taxes. */
33
	public $shipping_taxes			 = array();
34
35
	/** @var array Stores the shipping classes. */
36
	public $shipping_classes		 = array();
37
38
	/** @var array Stores packages to ship and to get quotes for. */
39
	public $packages				 = array();
40
41
	/**
42
	 * @var WC_Shipping The single instance of the class
43
	 * @since 2.1
44
	 */
45
	protected static $_instance = null;
46
47
	/**
48
	 * Main WC_Shipping Instance.
49
	 *
50
	 * Ensures only one instance of WC_Shipping is loaded or can be loaded.
51
	 *
52
	 * @since 2.1
53
	 * @static
54
	 * @return WC_Shipping Main instance
55
	 */
56
	public static function instance() {
57
		if ( is_null( self::$_instance ) ) {
58
			self::$_instance = new self();
59
		}
60
		return self::$_instance;
61
	}
62
63
	/**
64
	 * Cloning is forbidden.
65
	 *
66
	 * @since 2.1
67
	 */
68
	public function __clone() {
69
		_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce' ), '2.1' );
70
	}
71
72
	/**
73
	 * Unserializing instances of this class is forbidden.
74
	 *
75
	 * @since 2.1
76
	 */
77
	public function __wakeup() {
78
		_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce' ), '2.1' );
79
	}
80
81
	/**
82
	 * Initialize shipping.
83
	 */
84
	public function __construct() {
85
		$this->enabled = wc_shipping_enabled();
86
87
		if ( $this->enabled ) {
88
			$this->init();
89
		}
90
	}
91
92
    /**
93
     * Initialize shipping.
94
     */
95
    public function init() {
96
		do_action( 'woocommerce_shipping_init' );
97
	}
98
99
	/**
100
	 * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter.
101
	 * @return array
102
	 */
103
	public function get_shipping_method_class_names() {
104
		// Unique Method ID => Method Class name
105
		$shipping_methods = array(
106
			'flat_rate'     => 'WC_Shipping_Flat_Rate',
107
			'free_shipping' => 'WC_Shipping_Free_Shipping',
108
			'local_pickup'  => 'WC_Shipping_Local_Pickup',
109
		);
110
111
		// For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here
112
		$maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );
113
114
		foreach ( $maybe_load_legacy_methods as $method ) {
115
			$options = get_option( 'woocommerce_' . $method . '_settings' );
116
			if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
117
				$shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method;
118
			}
119
		}
120
121
		return apply_filters( 'woocommerce_shipping_methods', $shipping_methods );
122
	}
123
124
	/**
125
	 * Loads all shipping methods which are hooked in. If a $package is passed some methods may add themselves conditionally.
126
	 *
127
	 * Loads all shipping methods which are hooked in.
128
	 * If a $package is passed some methods may add themselves conditionally and zones will be used.
129
	 *
130
	 * @param array $package
131
	 * @return array
132
	 */
133
	public function load_shipping_methods( $package = array() ) {
134
		if ( $package ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $package of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
135
			$shipping_zone          = WC_Shipping_Zones::get_zone_matching_package( $package );
0 ignored issues
show
Documentation introduced by
$package is of type array, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
136
			$this->shipping_methods = $shipping_zone->get_shipping_methods( true );
137
		} else {
138
			$this->shipping_methods = array();
139
		}
140
141
		// For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here.
142
		foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
143
			$this->register_shipping_method( $method_class );
144
		}
145
146
		// Methods can register themselves manually through this hook if necessary.
147
		do_action( 'woocommerce_load_shipping_methods', $package );
148
149
		// Return loaded methods
150
		return $this->get_shipping_methods();
151
	}
152
153
	/**
154
	 * Register a shipping method.
155
	 *
156
	 * @param object|string $method Either the name of the method's class, or an instance of the method's class.
157
	 */
158
	public function register_shipping_method( $method ) {
159
		if ( ! is_object( $method ) ) {
160
			if ( ! class_exists( $method ) ) {
161
				return false;
162
			}
163
			$method = new $method();
164
		}
165
		$this->shipping_methods[ $method->id ] = $method;
166
	}
167
168
	/**
169
	 * Unregister shipping methods.
170
	 */
171
	public function unregister_shipping_methods() {
172
		$this->shipping_methods = array();
173
	}
174
175
	/**
176
	 * Returns all registered shipping methods for usage.
177
	 *
178
	 * @access public
179
	 * @return array
180
	 */
181
	public function get_shipping_methods() {
182
		if ( is_null( $this->shipping_methods ) ) {
183
			$this->load_shipping_methods();
184
		}
185
		return $this->shipping_methods;
186
	}
187
188
	/**
189
	 * Get an array of shipping classes.
190
	 *
191
	 * @access public
192
	 * @return array
193
	 */
194
	public function get_shipping_classes() {
195
		if ( empty( $this->shipping_classes ) ) {
196
			$classes                = get_terms( 'product_shipping_class', array( 'hide_empty' => '0', 'orderby' => 'name' ) );
197
			$this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array();
198
		}
199
		return $this->shipping_classes;
200
	}
201
202
	/**
203
	 * Get the default method.
204
	 * @param  array  $available_methods
205
	 * @param  boolean $current_chosen_method
206
	 * @return string
207
	 */
208
	private function get_default_method( $available_methods, $current_chosen_method = false ) {
209
		if ( ! empty( $available_methods ) ) {
210
			if ( ! empty( $current_chosen_method ) ) {
211
				if ( isset( $available_methods[ $current_chosen_method ] ) ) {
212
					return $available_methods[ $current_chosen_method ]->id;
213
				} else {
214
					foreach ( $available_methods as $method_key => $method ) {
215
						if ( strpos( $method->id, $current_chosen_method ) === 0 ) {
216
							return $method->id;
217
						}
218
					}
219
				}
220
			}
221
			return current( $available_methods )->id;
222
		}
223
		return '';
224
	}
225
226
	/**
227
	 * Calculate shipping for (multiple) packages of cart items.
228
	 *
229
	 * @param array $packages multi-dimensional array of cart items to calc shipping for
230
	 */
231
	public function calculate_shipping( $packages = array() ) {
232
		$this->shipping_total = null;
233
		$this->shipping_taxes = array();
234
		$this->packages       = array();
235
236
		if ( ! $this->enabled || empty( $packages ) ) {
237
			return;
238
		}
239
240
		// Calculate costs for passed packages
241
		foreach ( $packages as $package_key => $package ) {
242
			$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package );
243
		}
244
245
		/**
246
		 * Allow packages to be reorganized after calculate the shipping.
247
		 *
248
		 * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages
249
		 * but before Woocommerce does anything with them. A good example of usage is to merge the shipping methods for multiple
250
		 * packages for marketplaces.
251
		 *
252
		 * @since 2.6.0
253
		 *
254
		 * @param array $packages The array of packages after shipping costs are calculated.
255
		 */
256
		$this->packages = apply_filters( 'woocommerce_shipping_packages', $this->packages );
257
258
		if ( ! is_array( $this->packages ) || empty( $this->packages ) ) {
259
			return;
260
		}
261
262
		// Get all chosen methods
263
		$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
264
		$method_counts  = WC()->session->get( 'shipping_method_counts' );
265
266
		// Get chosen methods for each package
267
		foreach ( $this->packages as $i => $package ) {
268
			$chosen_method    = false;
269
			$method_count     = false;
270
271
			if ( ! empty( $chosen_methods[ $i ] ) ) {
272
				$chosen_method = $chosen_methods[ $i ];
273
			}
274
275
			if ( ! empty( $method_counts[ $i ] ) ) {
276
				$method_count = absint( $method_counts[ $i ] );
277
			}
278
279
			if ( sizeof( $package['rates'] ) > 0 ) {
280
281
				// If not set, not available, or available methods have changed, set to the DEFAULT option
282
				if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || $method_count !== sizeof( $package['rates'] ) ) {
283
					$chosen_method        = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], $chosen_method ), $package['rates'] );
284
					$chosen_methods[ $i ] = $chosen_method;
285
					$method_counts[ $i ]  = sizeof( $package['rates'] );
286
					do_action( 'woocommerce_shipping_method_chosen', $chosen_method );
287
				}
288
289
				// Store total costs
290
				if ( $chosen_method ) {
291
					$rate = $package['rates'][ $chosen_method ];
292
293
					// Merge cost and taxes - label and ID will be the same
294
					$this->shipping_total += $rate->cost;
295
296
					if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) {
297 View Code Duplication
						foreach ( array_keys( $this->shipping_taxes + $rate->taxes ) as $key ) {
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...
298
							$this->shipping_taxes[ $key ] = ( isset( $rate->taxes[$key] ) ? $rate->taxes[$key] : 0 ) + ( isset( $this->shipping_taxes[$key] ) ? $this->shipping_taxes[$key] : 0 );
299
						}
300
					}
301
				}
302
			}
303
		}
304
305
		// Save all chosen methods (array)
306
		WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
307
		WC()->session->set( 'shipping_method_counts', $method_counts );
308
	}
309
310
	/**
311
	 * Calculate shipping rates for a package,
312
	 *
313
	 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
314
	 *
315
	 * @param array $package cart items
316
	 * @return array
317
	 */
318
	public function calculate_shipping_for_package( $package = array() ) {
319
		if ( ! $this->enabled || ! $package ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $package of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
320
			return false;
321
		}
322
323
		// Check if we need to recalculate shipping for this package
324
		$package_hash   = 'wc_ship_' . md5( json_encode( $package ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
325
		$status_options = get_option( 'woocommerce_status_options', array() );
326
		$stored_rates   = WC()->session->get( 'shipping_for_package' );
327
328
		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || ! empty( $status_options['shipping_debug_mode'] ) ) {
329
			// Calculate shipping method rates
330
			$package['rates'] = array();
331
332
			foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) {
0 ignored issues
show
Bug introduced by
The expression $this->load_shipping_methods($package) of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
333
				// Shipping instances need an ID
334
				if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
335
					$package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); // + instead of array_merge maintains numeric keys
336
				}
337
			}
338
339
			// Filter the calculated rates
340
			$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
341
342
			// Store in session to avoid recalculation
343
			WC()->session->set( 'shipping_for_package', array(
344
				'package_hash' => $package_hash,
345
				'rates'        => $package['rates']
346
			) );
347
		} else {
348
			$package['rates'] = $stored_rates['rates'];
349
		}
350
351
		return $package;
352
	}
353
354
	/**
355
	 * Get packages.
356
	 * @return array
357
	 */
358
	public function get_packages() {
359
		return $this->packages;
360
	}
361
362
	/**
363
	 * Reset shipping.
364
	 *
365
	 * Reset the totals for shipping as a whole.
366
	 */
367
	public function reset_shipping() {
368
		unset( WC()->session->chosen_shipping_methods );
369
		$this->shipping_total = null;
370
		$this->shipping_taxes = array();
371
		$this->packages = array();
372
	}
373
374
	/**
375
	 * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused.
376
	 */
377
	public function sort_shipping_methods() {
378
		_deprecated_function( 'sort_shipping_methods', '2.6', '' );
379
		return $this->shipping_methods;
380
	}
381
}
382