Completed
Pull Request — master (#9826)
by Mike
20:41
created

WC_Shipping::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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 = apply_filters( 'woocommerce_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 $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();
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
<<<<<<< HEAD
177
	 * Returns all registered shipping methods.
178
=======
179
	 * Sort shipping methods.
180
	 *
181
	 * Sorts shipping methods into the user defined order.
182
	 *
183
	 * @return array
184
	 */
185
	public function sort_shipping_methods() {
186
187
		$sorted_shipping_methods = array();
188
189
		// Get order option
190
		$ordering 	= (array) get_option('woocommerce_shipping_method_order');
191
		$order_end 	= 999;
192
193
		// Load shipping methods in order
194
		foreach ( $this->shipping_methods as $method ) {
195
196
			if ( isset( $ordering[ $method->id ] ) && is_numeric( $ordering[ $method->id ] ) ) {
197
				// Add in position
198
				$sorted_shipping_methods[ $ordering[ $method->id ] ][] = $method;
199
			} else {
200
				// Add to end of the array
201
				$sorted_shipping_methods[ $order_end ][] = $method;
202
			}
203
		}
204
205
		ksort( $sorted_shipping_methods );
206
207
		$this->shipping_methods = array();
208
209
		foreach ( $sorted_shipping_methods as $methods )
210
			foreach ( $methods as $method ) {
211
				$id = empty( $method->instance_id ) ? $method->id : $method->instance_id;
212
				$this->shipping_methods[ $id ] = $method;
213
			}
214
215
		return $this->shipping_methods;
216
	}
217
218
	/**
219
	 * Returns all registered shipping methods for usage.
220
	 *
221
	 * @access public
222
	 * @return array
223
	 */
224
	public function get_shipping_methods() {
225
		if ( is_null( $this->shipping_methods ) ) {
226
			$this->load_shipping_methods();
227
		}
228
		return $this->shipping_methods;
229
	}
230
231
	/**
232
	 * Get an array of shipping classes.
233
	 *
234
	 * @access public
235
	 * @return array
236
	 */
237
	public function get_shipping_classes() {
238
		if ( empty( $this->shipping_classes ) ) {
239
			$classes                = get_terms( 'product_shipping_class', array( 'hide_empty' => '0' ) );
240
			$this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array();
241
		}
242
		return $this->shipping_classes;
243
	}
244
245
	/**
246
	 * Get the default method.
247
	 * @param  array  $available_methods
248
	 * @param  boolean $current_chosen_method
249
	 * @return string
250
	 */
251
	private function get_default_method( $available_methods, $current_chosen_method = false ) {
252
		if ( ! empty( $available_methods ) ) {
253
			if ( ! empty( $current_chosen_method ) ) {
254
				if ( isset( $available_methods[ $current_chosen_method ] ) ) {
255
					return $available_methods[ $current_chosen_method ]->id;
256
				} else {
257
					foreach ( $available_methods as $method_key => $method ) {
258
						if ( strpos( $method->id, $current_chosen_method ) === 0 ) {
259
							return $method->id;
260
						}
261
					}
262
				}
263
			}
264
			return current( $available_methods )->id;
265
		}
266
		return '';
267
	}
268
269
	/**
270
	 * Calculate shipping for (multiple) packages of cart items.
271
	 *
272
	 * @param array $packages multi-dimensional array of cart items to calc shipping for
273
	 */
274
	public function calculate_shipping( $packages = array() ) {
275
		$this->shipping_total = null;
276
		$this->shipping_taxes = array();
277
		$this->packages       = array();
278
279
		if ( ! $this->enabled || empty( $packages ) ) {
280
			return;
281
		}
282
283
		// Calculate costs for passed packages
284
		foreach ( $packages as $package_key => $package ) {
285
			$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package );
286
		}
287
288
		// Get all chosen methods
289
		$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
290
		$method_counts  = WC()->session->get( 'shipping_method_counts' );
291
292
		// Get chosen methods for each package
293
		foreach ( $this->packages as $i => $package ) {
294
			$chosen_method    = false;
295
			$method_count     = false;
296
297
			if ( ! empty( $chosen_methods[ $i ] ) ) {
298
				$chosen_method = $chosen_methods[ $i ];
299
			}
300
301
			if ( ! empty( $method_counts[ $i ] ) ) {
302
				$method_count = absint( $method_counts[ $i ] );
303
			}
304
305
			if ( sizeof( $package['rates'] ) > 0 ) {
306
307
				// If not set, not available, or available methods have changed, set to the DEFAULT option
308
				if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || $method_count !== sizeof( $package['rates'] ) ) {
309
					$chosen_method        = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], $chosen_method ), $package['rates'] );
310
					$chosen_methods[ $i ] = $chosen_method;
311
					$method_counts[ $i ]  = sizeof( $package['rates'] );
312
					do_action( 'woocommerce_shipping_method_chosen', $chosen_method );
313
				}
314
315
				// Store total costs
316
				if ( $chosen_method ) {
317
					$rate = $package['rates'][ $chosen_method ];
318
319
					// Merge cost and taxes - label and ID will be the same
320
					$this->shipping_total += $rate->cost;
321
322
					if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) {
323 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...
324
							$this->shipping_taxes[ $key ] = ( isset( $rate->taxes[$key] ) ? $rate->taxes[$key] : 0 ) + ( isset( $this->shipping_taxes[$key] ) ? $this->shipping_taxes[$key] : 0 );
325
						}
326
					}
327
				}
328
			}
329
		}
330
331
		// Save all chosen methods (array)
332
		WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
333
		WC()->session->set( 'shipping_method_counts', $method_counts );
334
	}
335
336
	/**
337
	 * Calculate shipping rates for a package,
338
	 *
339
	 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
340
	 *
341
	 * @param array $package cart items
342
	 * @return array
343
	 */
344
	public function calculate_shipping_for_package( $package = array() ) {
345
		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...
346
			return false;
347
		}
348
349
		// Check if we need to recalculate shipping for this package
350
		$package_hash   = 'wc_ship_' . md5( json_encode( $package ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
351
		$status_options = get_option( 'woocommerce_status_options', array() );
352
		$stored_rates   = WC()->session->get( 'shipping_for_package' );
353
354
		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || ! empty( $status_options['shipping_debug_mode'] ) ) {
355
			// Calculate shipping method rates
356
			$package['rates'] = array();
357
358
			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...
359
				// Shipping instances need an ID
360
				if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
361
					$package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); // + instead of array_merge maintains numeric keys
362
				}
363
			}
364
365
			// Filter the calculated rates
366
			$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
367
368
			// Store in session to avoid recalculation
369
			WC()->session->set( 'shipping_for_package', array(
370
				'package_hash' => $package_hash,
371
				'rates'        => $package['rates']
372
			) );
373
		} else {
374
			$package['rates'] = $stored_rates['rates'];
375
		}
376
377
		return $package;
378
	}
379
380
	/**
381
	 * Get packages.
382
	 * @return array
383
	 */
384
	public function get_packages() {
385
		return $this->packages;
386
	}
387
388
	/**
389
	 * Reset shipping.
390
	 *
391
	 * Reset the totals for shipping as a whole.
392
	 */
393
	public function reset_shipping() {
394
		unset( WC()->session->chosen_shipping_methods );
395
		$this->shipping_total = null;
396
		$this->shipping_taxes = array();
397
		$this->packages = array();
398
	}
399
400
	/**
401
	 * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused.
402
	 */
403
	public function sort_shipping_methods() {
404
		_deprecated_function( 'sort_shipping_methods', '2.6', '' );
405
		return $this->shipping_methods;
406
	}
407
}
408