/includes/class-wc-shipping.php

  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. * WC_Shipping 
  19. */ 
  20. class WC_Shipping { 
  21.  
  22. /** @var bool True if shipping is enabled. */ 
  23. public $enabled = false; 
  24.  
  25. /** @var array|null Stores methods loaded into woocommerce. */ 
  26. public $shipping_methods = null; 
  27.  
  28. /** @var float Stores the cost of shipping */ 
  29. public $shipping_total = 0; 
  30.  
  31. /** @var array Stores an array of shipping taxes. */ 
  32. public $shipping_taxes = array(); 
  33.  
  34. /** @var array Stores the shipping classes. */ 
  35. public $shipping_classes = array(); 
  36.  
  37. /** @var array Stores packages to ship and to get quotes for. */ 
  38. public $packages = array(); 
  39.  
  40. /** 
  41. * @var WC_Shipping The single instance of the class 
  42. * @since 2.1 
  43. */ 
  44. protected static $_instance = null; 
  45.  
  46. /** 
  47. * Main WC_Shipping Instance. 
  48. * 
  49. * Ensures only one instance of WC_Shipping is loaded or can be loaded. 
  50. * 
  51. * @since 2.1 
  52. * @static 
  53. * @return WC_Shipping Main instance 
  54. */ 
  55. public static function instance() { 
  56. if ( is_null( self::$_instance ) ) { 
  57. self::$_instance = new self(); 
  58. return self::$_instance; 
  59.  
  60. /** 
  61. * Cloning is forbidden. 
  62. * 
  63. * @since 2.1 
  64. */ 
  65. public function __clone() { 
  66. wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); 
  67.  
  68. /** 
  69. * Unserializing instances of this class is forbidden. 
  70. * 
  71. * @since 2.1 
  72. */ 
  73. public function __wakeup() { 
  74. wc_doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'woocommerce' ), '2.1' ); 
  75.  
  76. /** 
  77. * Initialize shipping. 
  78. */ 
  79. public function __construct() { 
  80. $this->enabled = wc_shipping_enabled(); 
  81.  
  82. if ( $this->enabled ) { 
  83. $this->init(); 
  84.  
  85. /** 
  86. * Initialize shipping. 
  87. */ 
  88. public function init() { 
  89. do_action( 'woocommerce_shipping_init' ); 
  90.  
  91. /** 
  92. * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. 
  93. * @return array 
  94. */ 
  95. public function get_shipping_method_class_names() { 
  96. // Unique Method ID => Method Class name 
  97. $shipping_methods = array( 
  98. 'flat_rate' => 'WC_Shipping_Flat_Rate',  
  99. 'free_shipping' => 'WC_Shipping_Free_Shipping',  
  100. 'local_pickup' => 'WC_Shipping_Local_Pickup',  
  101. ); 
  102.  
  103. // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here 
  104. $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); 
  105.  
  106. foreach ( $maybe_load_legacy_methods as $method ) { 
  107. $options = get_option( 'woocommerce_' . $method . '_settings' ); 
  108. if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { 
  109. $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; 
  110.  
  111. return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); 
  112.  
  113. /** 
  114. * Loads all shipping methods which are hooked in. If a $package is passed some methods may add themselves conditionally. 
  115. * 
  116. * Loads all shipping methods which are hooked in. 
  117. * If a $package is passed some methods may add themselves conditionally and zones will be used. 
  118. * 
  119. * @param array $package 
  120. * @return array 
  121. */ 
  122. public function load_shipping_methods( $package = array() ) { 
  123. if ( ! empty( $package ) ) { 
  124. $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); 
  125. $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); 
  126. $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); 
  127.  
  128. // Debug output 
  129. if ( $debug_mode && ! defined( 'WOOCOMMERCE_CHECKOUT' ) && ! wc_has_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' ) ) { 
  130. wc_add_notice( 'Customer matched zone "' . $shipping_zone->get_zone_name() . '"' ); 
  131. } else { 
  132. $this->shipping_methods = array(); 
  133.  
  134. // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. 
  135. foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { 
  136. $this->register_shipping_method( $method_class ); 
  137.  
  138. // Methods can register themselves manually through this hook if necessary. 
  139. do_action( 'woocommerce_load_shipping_methods', $package ); 
  140.  
  141. // Return loaded methods 
  142. return $this->get_shipping_methods(); 
  143.  
  144. /** 
  145. * Register a shipping method. 
  146. * 
  147. * @param object|string $method Either the name of the method's class, or an instance of the method's class. 
  148. */ 
  149. public function register_shipping_method( $method ) { 
  150. if ( ! is_object( $method ) ) { 
  151. if ( ! class_exists( $method ) ) { 
  152. return false; 
  153. $method = new $method(); 
  154. if ( is_null( $this->shipping_methods ) ) { 
  155. $this->shipping_methods = array(); 
  156. $this->shipping_methods[ $method->id ] = $method; 
  157.  
  158. /** 
  159. * Unregister shipping methods. 
  160. */ 
  161. public function unregister_shipping_methods() { 
  162. $this->shipping_methods = null; 
  163.  
  164. /** 
  165. * Returns all registered shipping methods for usage. 
  166. * 
  167. * @access public 
  168. * @return array 
  169. */ 
  170. public function get_shipping_methods() { 
  171. if ( is_null( $this->shipping_methods ) ) { 
  172. $this->load_shipping_methods(); 
  173. return $this->shipping_methods; 
  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', 'orderby' => 'name' ) ); 
  184. $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); 
  185. return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); 
  186.  
  187. /** 
  188. * Get the default method. 
  189. * @param array $available_methods 
  190. * @param boolean $current_chosen_method 
  191. * @return string 
  192. */ 
  193. private function get_default_method( $available_methods, $current_chosen_method = false ) { 
  194. if ( ! empty( $available_methods ) ) { 
  195. if ( ! empty( $current_chosen_method ) ) { 
  196. if ( isset( $available_methods[ $current_chosen_method ] ) ) { 
  197. return $available_methods[ $current_chosen_method ]->id; 
  198. } else { 
  199. foreach ( $available_methods as $method_key => $method ) { 
  200. if ( strpos( $method->id, $current_chosen_method ) === 0 ) { 
  201. return $method->id; 
  202. return current( $available_methods )->id; 
  203. return ''; 
  204.  
  205. /** 
  206. * Calculate shipping for (multiple) packages of cart items. 
  207. * 
  208. * @param array $packages multi-dimensional array of cart items to calc shipping for 
  209. */ 
  210. public function calculate_shipping( $packages = array() ) { 
  211. $this->shipping_total = 0; 
  212. $this->shipping_taxes = array(); 
  213. $this->packages = array(); 
  214.  
  215. if ( ! $this->enabled || empty( $packages ) ) { 
  216. return; 
  217.  
  218. // Calculate costs for passed packages 
  219. foreach ( $packages as $package_key => $package ) { 
  220. $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); 
  221.  
  222. /** 
  223. * Allow packages to be reorganized after calculate the shipping. 
  224. * 
  225. * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages 
  226. * but before Woocommerce does anything with them. A good example of usage is to merge the shipping methods for multiple 
  227. * packages for marketplaces. 
  228. * 
  229. * @since 2.6.0 
  230. * 
  231. * @param array $packages The array of packages after shipping costs are calculated. 
  232. */ 
  233. $this->packages = apply_filters( 'woocommerce_shipping_packages', $this->packages ); 
  234.  
  235. if ( ! is_array( $this->packages ) || empty( $this->packages ) ) { 
  236. return; 
  237.  
  238. // Get all chosen methods 
  239. $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); 
  240. $method_counts = WC()->session->get( 'shipping_method_counts' ); 
  241.  
  242. // Get chosen methods for each package 
  243. foreach ( $this->packages as $i => $package ) { 
  244. $chosen_method = false; 
  245. $method_count = false; 
  246.  
  247. if ( ! empty( $chosen_methods[ $i ] ) ) { 
  248. $chosen_method = $chosen_methods[ $i ]; 
  249.  
  250. if ( ! empty( $method_counts[ $i ] ) ) { 
  251. $method_count = absint( $method_counts[ $i ] ); 
  252.  
  253. if ( sizeof( $package['rates'] ) > 0 ) { 
  254.  
  255. // If not set, not available, or available methods have changed, set to the DEFAULT option 
  256. if ( empty( $chosen_method ) || ! isset( $package['rates'][ $chosen_method ] ) || sizeof( $package['rates'] ) !== $method_count ) { 
  257. $chosen_method = apply_filters( 'woocommerce_shipping_chosen_method', $this->get_default_method( $package['rates'], false ), $package['rates'], $chosen_method ); 
  258. $chosen_methods[ $i ] = $chosen_method; 
  259. $method_counts[ $i ] = sizeof( $package['rates'] ); 
  260. do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); 
  261.  
  262. // Store total costs 
  263. if ( $chosen_method && isset( $package['rates'][ $chosen_method ] ) ) { 
  264. $rate = $package['rates'][ $chosen_method ]; 
  265.  
  266. // Merge cost and taxes - label and ID will be the same 
  267. $this->shipping_total += $rate->cost; 
  268.  
  269. if ( ! empty( $rate->taxes ) && is_array( $rate->taxes ) ) { 
  270. foreach ( array_keys( $this->shipping_taxes + $rate->taxes ) as $key ) { 
  271. $this->shipping_taxes[ $key ] = ( isset( $rate->taxes[ $key ] ) ? $rate->taxes[ $key ] : 0 ) + ( isset( $this->shipping_taxes[ $key ] ) ? $this->shipping_taxes[ $key ] : 0 ); 
  272.  
  273. // Save all chosen methods (array) 
  274. WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); 
  275. WC()->session->set( 'shipping_method_counts', $method_counts ); 
  276.  
  277. /** 
  278. * See if package is shippable. 
  279. * @param array $package 
  280. * @return boolean 
  281. */ 
  282. protected function is_package_shippable( $package ) { 
  283. $allowed = array_keys( WC()->countries->get_shipping_countries() ); 
  284. return in_array( $package['destination']['country'], $allowed ); 
  285.  
  286. /** 
  287. * Calculate shipping rates for a package,  
  288. * 
  289. * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. 
  290. * 
  291. * @param array $package cart items 
  292. * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. 
  293. * @return array 
  294. */ 
  295. public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { 
  296. if ( ! $this->enabled || empty( $package ) || ! $this->is_package_shippable( $package ) ) { 
  297. return false; 
  298.  
  299. // Check if we need to recalculate shipping for this package 
  300. $package_to_hash = $package; 
  301.  
  302. // Remove data objects so hashes are consistent 
  303. foreach ( $package_to_hash['contents'] as $item_id => $item ) { 
  304. unset( $package_to_hash['contents'][ $item_id ]['data'] ); 
  305.  
  306. $package_hash = 'wc_ship_' . md5( json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); 
  307. $session_key = 'shipping_for_package_' . $package_key; 
  308. $stored_rates = WC()->session->get( $session_key ); 
  309.  
  310. if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { 
  311. // Calculate shipping method rates 
  312. $package['rates'] = array(); 
  313.  
  314. foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { 
  315. // Shipping instances need an ID 
  316. if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) { 
  317. $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); // + instead of array_merge maintains numeric keys 
  318.  
  319. // Filter the calculated rates 
  320. $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); 
  321.  
  322. // Store in session to avoid recalculation 
  323. WC()->session->set( $session_key, array( 
  324. 'package_hash' => $package_hash,  
  325. 'rates' => $package['rates'],  
  326. ) ); 
  327. } else { 
  328. $package['rates'] = $stored_rates['rates']; 
  329.  
  330. return $package; 
  331.  
  332. /** 
  333. * Get packages. 
  334. * @return array 
  335. */ 
  336. public function get_packages() { 
  337. return $this->packages; 
  338.  
  339. /** 
  340. * Reset shipping. 
  341. * 
  342. * Reset the totals for shipping as a whole. 
  343. */ 
  344. public function reset_shipping() { 
  345. unset( WC()->session->chosen_shipping_methods ); 
  346. $this->shipping_total = 0; 
  347. $this->shipping_taxes = array(); 
  348. $this->packages = array(); 
  349.  
  350. /** 
  351. * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. 
  352. */ 
  353. public function sort_shipping_methods() { 
  354. wc_deprecated_function( 'sort_shipping_methods', '2.6' ); 
  355. return $this->shipping_methods; 
.