Completed
Push — master ( 747a6d...d4cc1f )
by Mike
07:43
created

WC_Shipping::is_package_shippable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
c 1
b 1
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
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|null 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 ( ! empty( $package ) ) {
135
			$status_options         = get_option( 'woocommerce_status_options', array() );
136
			$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...
137
			$this->shipping_methods = $shipping_zone->get_shipping_methods( true );
138
139
			// Debug output
140
			if ( ! empty( $status_options['shipping_debug_mode'] ) && ! defined( 'WOOCOMMERCE_CHECKOUT' ) && ! wc_has_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' ) ) {
141
				wc_add_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' );
142
			}
143
		} else {
144
			$this->shipping_methods = array();
145
		}
146
147
		// For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here.
148
		foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
149
			$this->register_shipping_method( $method_class );
150
		}
151
152
		// Methods can register themselves manually through this hook if necessary.
153
		do_action( 'woocommerce_load_shipping_methods', $package );
154
155
		// Return loaded methods
156
		return $this->get_shipping_methods();
157
	}
158
159
	/**
160
	 * Register a shipping method.
161
	 *
162
	 * @param object|string $method Either the name of the method's class, or an instance of the method's class.
163
	 */
164
	public function register_shipping_method( $method ) {
165
		if ( ! is_object( $method ) ) {
166
			if ( ! class_exists( $method ) ) {
167
				return false;
168
			}
169
			$method = new $method();
170
		}
171
		if ( is_null( $this->shipping_methods ) ) {
172
			$this->shipping_methods = array();
173
		}
174
		$this->shipping_methods[ $method->id ] = $method;
175
	}
176
177
	/**
178
	 * Unregister shipping methods.
179
	 */
180
	public function unregister_shipping_methods() {
181
		$this->shipping_methods = null;
182
	}
183
184
	/**
185
	 * Returns all registered shipping methods for usage.
186
	 *
187
	 * @access public
188
	 * @return array
189
	 */
190
	public function get_shipping_methods() {
191
		if ( is_null( $this->shipping_methods ) ) {
192
			$this->load_shipping_methods();
193
		}
194
		return $this->shipping_methods;
195
	}
196
197
	/**
198
	 * Get an array of shipping classes.
199
	 *
200
	 * @access public
201
	 * @return array
202
	 */
203
	public function get_shipping_classes() {
204
		if ( empty( $this->shipping_classes ) ) {
205
			$classes                = get_terms( 'product_shipping_class', array( 'hide_empty' => '0', 'orderby' => 'name' ) );
206
			$this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array();
207
		}
208
		return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes );
209
	}
210
211
	/**
212
	 * Get the default method.
213
	 * @param  array  $available_methods
214
	 * @param  boolean $current_chosen_method
215
	 * @return string
216
	 */
217
	private function get_default_method( $available_methods, $current_chosen_method = false ) {
218
		if ( ! empty( $available_methods ) ) {
219
			if ( ! empty( $current_chosen_method ) ) {
220
				if ( isset( $available_methods[ $current_chosen_method ] ) ) {
221
					return $available_methods[ $current_chosen_method ]->id;
222
				} else {
223
					foreach ( $available_methods as $method_key => $method ) {
224
						if ( strpos( $method->id, $current_chosen_method ) === 0 ) {
225
							return $method->id;
226
						}
227
					}
228
				}
229
			}
230
			return current( $available_methods )->id;
231
		}
232
		return '';
233
	}
234
235
	/**
236
	 * Calculate shipping for (multiple) packages of cart items.
237
	 *
238
	 * @param array $packages multi-dimensional array of cart items to calc shipping for
239
	 */
240
	public function calculate_shipping( $packages = array() ) {
241
		$this->shipping_total = null;
242
		$this->shipping_taxes = array();
243
		$this->packages       = array();
244
245
		if ( ! $this->enabled || empty( $packages ) ) {
246
			return;
247
		}
248
249
		// Calculate costs for passed packages
250
		foreach ( $packages as $package_key => $package ) {
251
			$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key );
252
		}
253
254
		/**
255
		 * Allow packages to be reorganized after calculate the shipping.
256
		 *
257
		 * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages
258
		 * but before Woocommerce does anything with them. A good example of usage is to merge the shipping methods for multiple
259
		 * packages for marketplaces.
260
		 *
261
		 * @since 2.6.0
262
		 *
263
		 * @param array $packages The array of packages after shipping costs are calculated.
264
		 */
265
		$this->packages = apply_filters( 'woocommerce_shipping_packages', $this->packages );
266
267
		if ( ! is_array( $this->packages ) || empty( $this->packages ) ) {
268
			return;
269
		}
270
271
		// Get all chosen methods
272
		$chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
273
		$method_counts  = WC()->session->get( 'shipping_method_counts' );
274
275
		// Get chosen methods for each package
276
		foreach ( $this->packages as $i => $package ) {
277
			$chosen_method    = false;
278
			$method_count     = false;
279
280
			if ( ! empty( $chosen_methods[ $i ] ) ) {
281
				$chosen_method = $chosen_methods[ $i ];
282
			}
283
284
			if ( ! empty( $method_counts[ $i ] ) ) {
285
				$method_count = absint( $method_counts[ $i ] );
286
			}
287
288
			if ( sizeof( $package['rates'] ) > 0 ) {
289
290
				// If not set, not available, or available methods have changed, set to the DEFAULT option
291
				if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || $method_count !== sizeof( $package['rates'] ) ) {
292
					$chosen_method        = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], false ), $package['rates'], $chosen_method );
293
					$chosen_methods[ $i ] = $chosen_method;
294
					$method_counts[ $i ]  = sizeof( $package['rates'] );
295
					do_action( 'woocommerce_shipping_method_chosen', $chosen_method );
296
				}
297
298
				// Store total costs
299
				if ( $chosen_method ) {
300
					$rate = $package['rates'][ $chosen_method ];
301
302
					// Merge cost and taxes - label and ID will be the same
303
					$this->shipping_total += $rate->cost;
304
305
					if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) {
306 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...
307
							$this->shipping_taxes[ $key ] = ( isset( $rate->taxes[$key] ) ? $rate->taxes[$key] : 0 ) + ( isset( $this->shipping_taxes[$key] ) ? $this->shipping_taxes[$key] : 0 );
308
						}
309
					}
310
				}
311
			}
312
		}
313
314
		// Save all chosen methods (array)
315
		WC()->session->set( 'chosen_shipping_methods', $chosen_methods );
316
		WC()->session->set( 'shipping_method_counts', $method_counts );
317
	}
318
319
	/**
320
	 * See if package is shippable.
321
	 * @param  array  $package
322
	 * @return boolean
323
	 */
324
	protected function is_package_shippable( $package ) {
325
		$allowed = array_keys( WC()->countries->get_shipping_countries() );
326
		return in_array( $package['destination']['country'], $allowed );
327
	}
328
329
	/**
330
	 * Calculate shipping rates for a package,
331
	 *
332
	 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
333
	 *
334
	 * @param array $package cart items
335
	 * @param int   $package_key Index of the package being calculated. Used to cache multiple package rates.
336
	 * @return array
337
	 */
338
	public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) {
339
		if ( ! $this->enabled || empty( $package ) || ! $this->is_package_shippable( $package ) ) {
340
			return false;
341
		}
342
343
		// Check if we need to recalculate shipping for this package
344
		$package_to_hash = $package;
345
346
		// Remove data objects so hashes are consistent
347
		foreach ( $package_to_hash['contents'] as $item_id => $item ) {
348
			unset( $package_to_hash['contents'][ $item_id ]['data'] );
349
		}
350
351
		$package_hash   = 'wc_ship_' . md5( json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
352
		$status_options = get_option( 'woocommerce_status_options', array() );
353
		$session_key    = 'shipping_for_package_' . $package_key;
354
		$stored_rates   = WC()->session->get( $session_key );
355
356
		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || ! empty( $status_options['shipping_debug_mode'] ) ) {
357
			// Calculate shipping method rates
358
			$package['rates'] = array();
359
360
			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...
361
				// Shipping instances need an ID
362
				if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
363
					$package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); // + instead of array_merge maintains numeric keys
364
				}
365
			}
366
367
			// Filter the calculated rates
368
			$package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package );
369
370
			// Store in session to avoid recalculation
371
			WC()->session->set( $session_key, array(
372
				'package_hash' => $package_hash,
373
				'rates'        => $package['rates']
374
			) );
375
		} else {
376
			$package['rates'] = $stored_rates['rates'];
377
		}
378
379
		return $package;
380
	}
381
382
	/**
383
	 * Get packages.
384
	 * @return array
385
	 */
386
	public function get_packages() {
387
		return $this->packages;
388
	}
389
390
	/**
391
	 * Reset shipping.
392
	 *
393
	 * Reset the totals for shipping as a whole.
394
	 */
395
	public function reset_shipping() {
396
		unset( WC()->session->chosen_shipping_methods );
397
		$this->shipping_total = null;
398
		$this->shipping_taxes = array();
399
		$this->packages = array();
400
	}
401
402
	/**
403
	 * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused.
404
	 */
405
	public function sort_shipping_methods() {
406
		_deprecated_function( 'sort_shipping_methods', '2.6', '' );
407
		return $this->shipping_methods;
408
	}
409
}
410