Completed
Pull Request — master (#9826)
by Mike
21:27
created

WC_Shipping   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 331
Duplicated Lines 0.91 %

Coupling/Cohesion

Components 3
Dependencies 5
Metric Value
wmc 54
lcom 3
cbo 5
dl 3
loc 331
rs 7.0642

17 Methods

Rating   Name   Duplication   Size   Complexity  
A instance() 0 6 2
A __clone() 0 3 1
A __wakeup() 0 3 1
A __construct() 0 7 2
A init() 0 3 1
A get_shipping_method_class_names() 0 10 1
A load_shipping_methods() 0 19 3
A register_shipping_method() 0 6 2
A unregister_shipping_methods() 0 3 1
A get_shipping_methods() 0 6 2
A get_shipping_classes() 0 7 3
B get_default_method() 0 17 6
C calculate_shipping() 3 61 17
D calculate_shipping_for_package() 0 35 9
A get_packages() 0 3 1
A reset_shipping() 0 6 1
A sort_shipping_methods() 0 4 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_Shipping 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_Shipping, 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 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 = 'yes' === get_option( 'woocommerce_calc_shipping' );
86
87
		if ( $this->enabled ) {
88
			$this->init();
89
		}
90
	}
91
92
    /**
93
     * init function.
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
		return apply_filters( 'woocommerce_shipping_methods', array(
106
			'flat_rate'              => 'WC_Shipping_Flat_Rate',
107
			'free_shipping'          => 'WC_Shipping_Free_Shipping',
108
			'international_delivery' => 'WC_Shipping_International_Delivery',
109
			'local_delivery'         => 'WC_Shipping_Local_Delivery',
110
			'local_pickup'           => 'WC_Shipping_Local_Pickup'
111
		) );
112
	}
113
114
	/**
115
	 * Load shipping methods.
116
	 *
117
	 * Loads all shipping methods which are hooked in.
118
	 * If a $package is passed some methods may add themselves conditionally and zones will be used.
119
	 *
120
	 * @param array $package
121
	 * @return array
122
	 */
123
	public function load_shipping_methods( $package = array() ) {
124
		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...
125
			$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...
126
			$this->shipping_methods = $shipping_zone->get_shipping_methods();
127
		} else {
128
			$this->shipping_methods = array();
129
		}
130
131
		// For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here.
132
		foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
133
			$this->register_shipping_method( $method_class );
134
		}
135
136
		// Methods can register themselves manually through this hook if necessary.
137
		do_action( 'woocommerce_load_shipping_methods', $package );
138
139
		// Return loaded methods
140
		return $this->get_shipping_methods();
141
	}
142
143
	/**
144
	 * Register a shipping method.
145
	 *
146
	 * @param object|string $method Either the name of the method's class, or an instance of the method's class.
147
	 */
148
	public function register_shipping_method( $method ) {
149
		if ( ! is_object( $method ) ) {
150
			$method = new $method();
151
		}
152
		$this->shipping_methods[ $method->id ] = $method;
153
	}
154
155
	/**
156
	 * Unregister shipping methods.
157
	 */
158
	public function unregister_shipping_methods() {
159
		$this->shipping_methods = array();
160
	}
161
162
	/**
163
	 * Returns all registered shipping methods.
164
	 *
165
	 * @access public
166
	 * @return array
167
	 */
168
	public function get_shipping_methods() {
169
		if ( is_null( $this->shipping_methods ) ) {
170
			$this->load_shipping_methods();
171
		}
172
		return $this->shipping_methods;
173
	}
174
175
	/**
176
	 * Get an array of shipping classes.
177
	 *
178
	 * @access public
179
	 * @return array
180
	 */
181
	public function get_shipping_classes() {
182
		if ( empty( $this->shipping_classes ) ) {
183
			$classes                = get_terms( 'product_shipping_class', array( 'hide_empty' => '0' ) );
184
			$this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array();
185
		}
186
		return $this->shipping_classes;
187
	}
188
189
	/**
190
	 * Get the default method.
191
	 * @param  array  $available_methods
192
	 * @param  boolean $current_chosen_method
193
	 * @return string
194
	 */
195
	private function get_default_method( $available_methods, $current_chosen_method = false ) {
196
		if ( ! empty( $available_methods ) ) {
197
			if ( ! empty( $current_chosen_method ) ) {
198
				if ( isset( $available_methods[ $current_chosen_method ] ) ) {
199
					return $available_methods[ $current_chosen_method ]->id;
200
				} else {
201
					foreach ( $available_methods as $method_key => $method ) {
202
						if ( strpos( $method->id, $current_chosen_method ) === 0 ) {
203
							return $method->id;
204
						}
205
					}
206
				}
207
			}
208
			return current( $available_methods )->id;
209
		}
210
		return '';
211
	}
212
213
	/**
214
	 * Calculate shipping for (multiple) packages of cart items.
215
	 *
216
	 * @param array $packages multi-dimensional array of cart items to calc shipping for
217
	 */
218
	public function calculate_shipping( $packages = array() ) {
219
		$this->shipping_total = null;
220
		$this->shipping_taxes = array();
221
		$this->packages       = array();
222
223
		if ( ! $this->enabled || empty( $packages ) ) {
224
			return;
225
		}
226
227
		// Calculate costs for passed packages
228
		foreach ( $packages as $package_key => $package ) {
229
			$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package );
230
		}
231
232
		// Get all chosen methods
233
		$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
234
		$method_counts  = WC()->session->get( 'shipping_method_counts' );
235
236
		// Get chosen methods for each package
237
		foreach ( $this->packages as $i => $package ) {
238
			$chosen_method    = false;
239
			$method_count     = false;
240
241
			if ( ! empty( $chosen_methods[ $i ] ) ) {
242
				$chosen_method = $chosen_methods[ $i ];
243
			}
244
245
			if ( ! empty( $method_counts[ $i ] ) ) {
246
				$method_count = absint( $method_counts[ $i ] );
247
			}
248
249
			if ( sizeof( $package['rates'] ) > 0 ) {
250
251
				// If not set, not available, or available methods have changed, set to the DEFAULT option
252
				if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || $method_count !== sizeof( $package['rates'] ) ) {
253
					$chosen_method        = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], $chosen_method ), $package['rates'] );
254
					$chosen_methods[ $i ] = $chosen_method;
255
					$method_counts[ $i ]  = sizeof( $package['rates'] );
256
					do_action( 'woocommerce_shipping_method_chosen', $chosen_method );
257
				}
258
259
				// Store total costs
260
				if ( $chosen_method ) {
261
					$rate = $package['rates'][ $chosen_method ];
262
263
					// Merge cost and taxes - label and ID will be the same
264
					$this->shipping_total += $rate->cost;
265
266
					if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) {
267 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...
268
							$this->shipping_taxes[ $key ] = ( isset( $rate->taxes[$key] ) ? $rate->taxes[$key] : 0 ) + ( isset( $this->shipping_taxes[$key] ) ? $this->shipping_taxes[$key] : 0 );
269
						}
270
					}
271
				}
272
			}
273
		}
274
275
		// Save all chosen methods (array)
276
		WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
277
		WC()->session->set( 'shipping_method_counts', $method_counts );
278
	}
279
280
	/**
281
	 * Calculate shipping rates for a package,
282
	 *
283
	 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
284
	 *
285
	 * @param array $package cart items
286
	 * @return array
287
	 */
288
	public function calculate_shipping_for_package( $package = array() ) {
289
		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...
290
			return false;
291
		}
292
293
		// Check if we need to recalculate shipping for this package
294
		$package_hash   = 'wc_ship_' . md5( json_encode( $package ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
295
		$status_options = get_option( 'woocommerce_status_options', array() );
296
		$stored_rates   = WC()->session->get( 'shipping_for_package' );
297
298
		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || ! empty( $status_options['shipping_debug_mode'] ) ) {
299
			// Calculate shipping method rates
300
			$package['rates'] = array();
301
302
			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...
303
				// Shipping instances need an ID
304
				if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
305
					$package['rates'] = array_merge( $package['rates'], $shipping_method->get_rates_for_package( $package ) );
306
				}
307
			}
308
309
			// Filter the calculated rates
310
			$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
311
312
			// Store in session to avoid recalculation
313
			WC()->session->set( 'shipping_for_package', array(
314
				'package_hash' => $package_hash,
315
				'rates'        => $package['rates']
316
			) );
317
		} else {
318
			$package['rates'] = $stored_rates['rates'];
319
		}
320
321
		return $package;
322
	}
323
324
	/**
325
	 * Get packages.
326
	 * @return array
327
	 */
328
	public function get_packages() {
329
		return $this->packages;
330
	}
331
332
	/**
333
	 * Reset shipping.
334
	 *
335
	 * Reset the totals for shipping as a whole.
336
	 */
337
	public function reset_shipping() {
338
		unset( WC()->session->chosen_shipping_methods );
339
		$this->shipping_total = null;
340
		$this->shipping_taxes = array();
341
		$this->packages = array();
342
	}
343
344
	/**
345
	 * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused.
346
	 */
347
	public function sort_shipping_methods() {
348
		_deprecated_function( 'sort_shipping_methods', '2.6', '' );
349
		return $this->shipping_methods;
350
	}
351
}
352