PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / shipping / shipping.php

shipping.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/shipping/shipping.php

390 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * StoreEngine Shipping
4 *
5 * Handles shipping and loads shipping methods via hooks.
6 *
7 * @package StoreEngine
8 */
9
10 namespace StoreEngine\Shipping;
11
12 use StoreEngine\Classes\Cart;
13 use StoreEngine\Classes\Countries;
14 use StoreEngine\Classes\Exceptions\StoreEngineException;
15 use StoreEngine\Shipping\Methods\ShippingFlatRate;
16 use StoreEngine\Shipping\Methods\ShippingMethod;
17 use StoreEngine\Traits\Singleton;
18 use StoreEngine\Utils\Caching;
19 use StoreEngine\Utils\Helper;
20 use StoreEngine\Utils\ShippingUtils;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit; // Exit if accessed directly.
24 }
25
26 /**
27 * Core shipping engine.
28 */
29 final class Shipping {
30 use Singleton;
31
32 /**
33 * True if shipping is enabled.
34 *
35 * @var bool
36 */
37 public $enabled = false;
38
39 /**
40 * Stores methods loaded into StoreEngine.
41 *
42 * @var array|null
43 */
44 public $shipping_methods = null;
45
46 /**
47 * Stores the shipping classes.
48 *
49 * @var array
50 */
51 public array $shipping_classes = [];
52
53 /**
54 * Stores packages to ship and to get quotes for.
55 *
56 * @var array
57 */
58 public array $packages = [];
59
60 /**
61 * Magic getter.
62 *
63 * @param string $name Property name.
64 *
65 * @return mixed|void
66 */
67 public function __get( string $name ) {
68 // Grab from cart for backwards compatibility with versions prior to 3.2.
69 if ( 'shipping_total' === $name ) {
70 return Helper::cart()->get_shipping_total();
71 }
72
73 if ( 'shipping_taxes' === $name ) {
74 return Helper::cart()->get_shipping_taxes();
75 }
76 }
77
78 /**
79 * Initialize shipping.
80 */
81 public function __construct() {
82 $this->enabled = ShippingUtils::is_shipping_enabled();
83
84 if ( $this->enabled ) {
85 /**
86 * Initialize shipping.
87 */
88 do_action( 'storeengine/shipping_init' );
89 }
90 }
91
92 /**
93 * @return ShippingMethod[]
94 */
95 public function get_all_shipping_methods(): array {
96 static $shipping_methods = [];
97
98 if ( empty( $shipping_methods ) ) {
99 foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
100 if ( ! is_object( $method_class ) ) {
101 if ( ! class_exists( $method_class ) ) {
102 continue;
103 }
104 $method = new $method_class();
105 if ( is_null( $shipping_methods ) ) {
106 $shipping_methods = [];
107 }
108 $shipping_methods[ $method->get_id() ] = $method;
109 }
110 }
111 }
112
113 return $shipping_methods;
114 }
115
116 /**
117 * Shipping methods register themselves by returning their main class name through the shipping-methods registration filter.
118 *
119 * @return array
120 */
121 public function get_shipping_method_class_names(): array {
122 // Unique Method ID => Method Class name.
123 $shipping_methods = [
124 'flat_rate' => ShippingFlatRate::class,
125 ];
126
127 return apply_filters( 'storeengine/shipping/methods', $shipping_methods );
128 }
129
130 /**
131 * Loads all shipping methods which are hooked in.
132 * If a $package is passed, some methods may add themselves conditionally and zones will be used.
133 *
134 * @param array $package Package information.
135 *
136 * @return ShippingMethod[]
137 */
138 public function load_shipping_methods( $package = [] ) {
139 if ( ! empty( $package ) ) {
140 try {
141 $shipping_zone = ShippingZones::get_zone_matching_package( $package );
142 $this->shipping_methods = $shipping_zone->get_shipping_methods( true );
143 } catch ( StoreEngineException $e ) {
144 // mostly if zone is not in db (maybe deleted).
145 // @TODO add error logger.
146 $this->shipping_methods = [];
147 }
148 } else {
149 $this->shipping_methods = [];
150 }
151
152 // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here.
153 foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) {
154 $this->register_shipping_method( $method_class );
155 }
156
157 // Methods can register themselves manually through this hook if necessary.
158 do_action( 'storeengine/shipping/load_shipping_methods', $package );
159
160 // Return loaded methods.
161 return $this->get_shipping_methods();
162 }
163
164 /**
165 * Register a shipping method.
166 *
167 * @param object|string $method Either the name of the method's class, or an instance of the method's class.
168 *
169 * @return bool|void
170 */
171 public function register_shipping_method( $method ) {
172 if ( ! is_object( $method ) ) {
173 if ( ! class_exists( $method ) ) {
174 return false;
175 }
176 $method = new $method();
177 }
178 if ( is_null( $this->shipping_methods ) ) {
179 $this->shipping_methods = [];
180 }
181 $this->shipping_methods[ $method->get_id() ] = $method;
182 }
183
184 /**
185 * Unregister shipping methods.
186 */
187 public function unregister_shipping_methods() {
188 $this->shipping_methods = null;
189 }
190
191 /**
192 * Returns all registered shipping methods for usage.
193 *
194 * @return ShippingMethod[]
195 */
196 public function get_shipping_methods() {
197 if ( is_null( $this->shipping_methods ) ) {
198 $this->load_shipping_methods();
199 }
200
201 return $this->shipping_methods;
202 }
203
204 /**
205 * Get an array of shipping classes.
206 *
207 * @return array
208 */
209 public function get_shipping_classes(): array {
210 if ( empty( $this->shipping_classes ) ) {
211 $classes = get_terms( [
212 'taxonomy' => 'product_shipping_class',
213 'hide_empty' => false,
214 'orderby' => 'name',
215 ] );
216 $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : [];
217 }
218
219 return apply_filters( 'storeengine/shipping/get_shipping_classes', $this->shipping_classes );
220 }
221
222 /**
223 * Calculate shipping for (multiple) packages of cart items.
224 *
225 * @param array $packages multidimensional array of cart items to calc shipping for.
226 * @param ?Cart $cart Cart instance to avoid infinite looping.
227 *
228 * @return array Array of calculated packages.
229 */
230 public function calculate_shipping( array $packages = [], ?Cart $cart = null ): array {
231 $this->packages = [];
232
233 if ( ! $this->enabled || empty( $packages ) ) {
234 return [];
235 }
236
237 // Calculate costs for passed packages.
238 foreach ( $packages as $package_key => $package ) {
239 $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $cart, $package, $package_key );
240 }
241
242 /**
243 * Allow packages to be reorganized after calculating the shipping.
244 *
245 * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages
246 * but before StoreEngine does anything with them. A good example of usage is to merge the shipping methods for multiple
247 * packages for marketplaces.
248 *
249 * @param array $packages The array of packages after shipping costs are calculated.
250 */
251 $this->packages = array_filter( (array) apply_filters( 'storeengine/shipping/packages', $this->packages ) );
252
253 return $this->packages;
254 }
255
256 /**
257 * See if package is shippable.
258 *
259 * Packages are shippable until proven otherwise e.g. after getting a shipping country.
260 *
261 * @param array $package Package of cart items.
262 *
263 * @return bool
264 */
265 public function is_package_shippable( array $package ): bool {
266 // Packages are shippable until proven otherwise.
267 if ( empty( $package['destination']['country'] ) ) {
268 return true;
269 }
270
271 $allowed = array_keys( Countries::init()->get_shipping_countries() );
272
273 return in_array( $package['destination']['country'], $allowed, true );
274 }
275
276 /**
277 * Calculate shipping rates for a package,
278 *
279 * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load.
280 *
281 * @param Cart $cart Cart instance.
282 * @param array $package Package of cart items.
283 * @param int|string $package_key Index of the package being calculated. Used to cache multiple package rates.
284 *
285 * @return array|bool
286 */
287 public function calculate_shipping_for_package( Cart $cart, array $package = [], $package_key = 0 ) {
288 // If shipping is disabled or the package is invalid, return false.
289 if ( ! $this->enabled || empty( $package ) ) {
290 return false;
291 }
292
293 $package['rates'] = [];
294
295 // If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates.
296 if ( ! $this->is_package_shippable( $package ) ) {
297 return $package;
298 }
299
300 // Check if we need to recalculate shipping for this package.
301 $package_to_hash = $package;
302
303 // Remove data objects so hashes are consistent.
304 foreach ( $package_to_hash['contents'] as $item_id => $item ) {
305 if ( isset( $item->data ) ) {
306 unset( $package_to_hash['contents'][ $item_id ]->data );
307 }
308 }
309
310 // Get rates stored in the session data for this package.
311 $session_key = 'shipping_for_package_' . $package_key;
312 $stored_rates = $cart->get_meta( $session_key );
313
314 // Calculate the hash for this package so we can tell if it's changed since last calculation.
315 $package_hash = 'se_ship_' . md5( wp_json_encode( $package_to_hash ) . Caching::get_transient_version( 'shipping' ) );
316
317 if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] ) {
318 foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) {
319 if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) {
320 /**
321 * Fires before getting shipping rates for a package.
322 *
323 * @param array $package Package of cart items.
324 * @param ShippingMethod $shipping_method Shipping method instance.
325 */
326 do_action( 'storeengine/shipping/before_get_rates_for_package', $package, $shipping_method );
327
328 // Use + instead of array_merge to maintain numeric keys.
329 $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package );
330
331 /**
332 * Fires after getting shipping rates for a package.
333 *
334 * @param array $package Package of cart items.
335 * @param ShippingMethod $shipping_method Shipping method instance.
336 */
337 do_action( 'storeengine/shipping/after_get_rates_for_package', $package, $shipping_method );
338 }
339 }
340
341 /**
342 * Filter the calculated shipping rates.
343 *
344 * @param array $package ['rates'] Package rates.
345 * @param array $package Package of cart items.
346 */
347 $package['rates'] = apply_filters( 'storeengine/shipping/package_rates', $package['rates'], $package );
348
349 // Package rates should be an array, if it was filtered into a non-array, reset it. Don't reset to the
350 // unfiltered value, as e.g. a 3pd could have set it to "false" to remove rates.
351 if ( ! is_array( $package['rates'] ) ) {
352 $package['rates'] = [];
353 }
354
355 // Store in session to avoid recalculation.
356 $cart->set_meta(
357 $session_key,
358 array(
359 'package_hash' => $package_hash,
360 'rates' => $package['rates'],
361 )
362 );
363 } else {
364 $package['rates'] = $stored_rates['rates'];
365 }
366
367 return $package;
368 }
369
370 /**
371 * Get packages.
372 *
373 * @return array
374 */
375 public function get_packages(): array {
376 return $this->packages;
377 }
378
379 /**
380 * Reset shipping.
381 *
382 * Reset the totals for shipping as a whole.
383 */
384 public function reset_shipping() {
385 $this->packages = [];
386 }
387 }
388
389 // End of file shipping.php.
390