Completed
Push — master ( 15aa29...17da96 )
by Claudio
18:39 queued 11s
created

includes/abstracts/abstract-wc-shipping-method.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Abstract shipping method
4
 *
5
 * @class WC_Shipping_Method
6
 * @package WooCommerce/Abstracts
7
 */
8
9
if ( ! defined( 'ABSPATH' ) ) {
10
	exit;
11
}
12
13
/**
14
 * WooCommerce Shipping Method Class.
15
 *
16
 * Extended by shipping methods to handle shipping calculations etc.
17
 *
18
 * @class       WC_Shipping_Method
19
 * @version     3.0.0
20
 * @package     WooCommerce/Abstracts
21
 */
22
abstract class WC_Shipping_Method extends WC_Settings_API {
23
24
	/**
25
	 * Features this method supports. Possible features used by core:
26
	 * - shipping-zones Shipping zone functionality + instances
27
	 * - instance-settings Instance settings screens.
28
	 * - settings Non-instance settings screens. Enabled by default for BW compatibility with methods before instances existed.
29
	 * - instance-settings-modal Allows the instance settings to be loaded within a modal in the zones UI.
30
	 *
31
	 * @var array
32
	 */
33
	public $supports = array( 'settings' );
34
35
	/**
36
	 * Unique ID for the shipping method - must be set.
37
	 *
38
	 * @var string
39
	 */
40
	public $id = '';
41
42
	/**
43
	 * Method title.
44
	 *
45
	 * @var string
46
	 */
47
	public $method_title = '';
48
49
	/**
50
	 * Method description.
51
	 *
52
	 * @var string
53
	 */
54
	public $method_description = '';
55
56
	/**
57
	 * Yes or no based on whether the method is enabled.
58
	 *
59
	 * @var string
60
	 */
61
	public $enabled = 'yes';
62
63
	/**
64
	 * Shipping method title for the frontend.
65
	 *
66
	 * @var string
67
	 */
68
	public $title;
69
70
	/**
71
	 * This is an array of rates - methods must populate this array to register shipping costs.
72
	 *
73
	 * @var array
74
	 */
75
	public $rates = array();
76
77
	/**
78
	 * If 'taxable' tax will be charged for this method (if applicable).
79
	 *
80
	 * @var string
81
	 */
82
	public $tax_status = 'taxable';
83
84
	/**
85
	 * Fee for the method (if applicable).
86
	 *
87
	 * @var string
88
	 */
89
	public $fee = null;
90
91
	/**
92
	 * Minimum fee for the method (if applicable).
93
	 *
94
	 * @var string
95
	 */
96
	public $minimum_fee = null;
97
98
	/**
99
	 * Instance ID if used.
100
	 *
101
	 * @var int
102
	 */
103
	public $instance_id = 0;
104
105
	/**
106
	 * Instance form fields.
107
	 *
108
	 * @var array
109
	 */
110
	public $instance_form_fields = array();
111
112
	/**
113
	 * Instance settings.
114
	 *
115
	 * @var array
116
	 */
117
	public $instance_settings = array();
118
119
	/**
120
	 * Availability - legacy. Used for method Availability.
121
	 * No longer useful for instance based shipping methods.
122
	 *
123
	 * @deprecated 2.6.0
124
	 * @var string
125
	 */
126
	public $availability;
127
128
	/**
129
	 * Availability countries - legacy. Used for method Availability.
130
	 * No longer useful for instance based shipping methods.
131
	 *
132
	 * @deprecated 2.6.0
133
	 * @var array
134
	 */
135
	public $countries = array();
136
137
	/**
138
	 * Constructor.
139
	 *
140
	 * @param int $instance_id Instance ID.
141
	 */
142
	public function __construct( $instance_id = 0 ) {
143
		$this->instance_id = absint( $instance_id );
144
	}
145
146
	/**
147
	 * Check if a shipping method supports a given feature.
148
	 *
149
	 * Methods should override this to declare support (or lack of support) for a feature.
150
	 *
151
	 * @param string $feature The name of a feature to test support for.
152
	 * @return bool True if the shipping method supports the feature, false otherwise.
153
	 */
154 93
	public function supports( $feature ) {
155 93
		return apply_filters( 'woocommerce_shipping_method_supports', in_array( $feature, $this->supports ), $feature, $this );
156
	}
157
158
	/**
159
	 * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method.
160
	 *
161
	 * @param array $package Package array.
162
	 */
163
	public function calculate_shipping( $package = array() ) {}
164
165
	/**
166
	 * Whether or not we need to calculate tax on top of the shipping rate.
167
	 *
168
	 * @return boolean
169
	 */
170 10
	public function is_taxable() {
171 10
		return wc_tax_enabled() && 'taxable' === $this->tax_status && ( WC()->customer && ! WC()->customer->get_is_vat_exempt() );
172
	}
173
174
	/**
175
	 * Whether or not this method is enabled in settings.
176
	 *
177
	 * @since 2.6.0
178
	 * @return boolean
179
	 */
180 10
	public function is_enabled() {
181 10
		return 'yes' === $this->enabled;
182
	}
183
184
	/**
185
	 * Return the shipping method instance ID.
186
	 *
187
	 * @since 2.6.0
188
	 * @return int
189
	 */
190 16
	public function get_instance_id() {
191 16
		return $this->instance_id;
192
	}
193
194
	/**
195
	 * Return the shipping method title.
196
	 *
197
	 * @since 2.6.0
198
	 * @return string
199
	 */
200 427
	public function get_method_title() {
201 427
		return apply_filters( 'woocommerce_shipping_method_title', $this->method_title, $this );
202
	}
203
204
	/**
205
	 * Return the shipping method description.
206
	 *
207
	 * @since 2.6.0
208
	 * @return string
209
	 */
210
	public function get_method_description() {
211
		return apply_filters( 'woocommerce_shipping_method_description', $this->method_description, $this );
212
	}
213
214
	/**
215
	 * Return the shipping title which is user set.
216
	 *
217
	 * @return string
218
	 */
219
	public function get_title() {
220
		return apply_filters( 'woocommerce_shipping_method_title', $this->title, $this->id );
221
	}
222
223
	/**
224
	 * Return calculated rates for a package.
225
	 *
226
	 * @since 2.6.0
227
	 * @param array $package Package array.
228
	 * @return array
229
	 */
230 10
	public function get_rates_for_package( $package ) {
231 10
		$this->rates = array();
232 10
		if ( $this->is_available( $package ) && ( empty( $package['ship_via'] ) || in_array( $this->id, $package['ship_via'] ) ) ) {
233 10
			$this->calculate_shipping( $package );
234
		}
235 10
		return $this->rates;
236
	}
237
238
	/**
239
	 * Returns a rate ID based on this methods ID and instance, with an optional
240
	 * suffix if distinguishing between multiple rates.
241
	 *
242
	 * @since 2.6.0
243
	 * @param string $suffix Suffix.
244
	 * @return string
245
	 */
246 10
	public function get_rate_id( $suffix = '' ) {
247 10
		$rate_id = array( $this->id );
248
249 10
		if ( $this->instance_id ) {
250
			$rate_id[] = $this->instance_id;
251
		}
252
253 10
		if ( $suffix ) {
254
			$rate_id[] = $suffix;
255
		}
256
257 10
		return implode( ':', $rate_id );
258
	}
259
260
	/**
261
	 * Add a shipping rate. If taxes are not set they will be calculated based on cost.
262
	 *
263
	 * @param array $args Arguments (default: array()).
264
	 */
265 10
	public function add_rate( $args = array() ) {
266 10
		$args = apply_filters(
267 10
			'woocommerce_shipping_method_add_rate_args',
268 10
			wp_parse_args(
269 10
				$args,
270
				array(
271 10
					'id'        => $this->get_rate_id(), // ID for the rate. If not passed, this id:instance default will be used.
272 10
					'label'     => '', // Label for the rate.
273 10
					'cost'      => '0', // Amount or array of costs (per item shipping).
274 10
					'taxes'     => '', // Pass taxes, or leave empty to have it calculated for you, or 'false' to disable calculations.
275 10
					'calc_tax'  => 'per_order', // Calc tax per_order or per_item. Per item needs an array of costs.
276
					'meta_data' => array(), // Array of misc meta data to store along with this rate - key value pairs.
277
					'package'   => false, // Package array this rate was generated for @since 2.6.0.
278 10
					'price_decimals' => wc_get_price_decimals(),
279
				)
280
			),
281 10
			$this
282
		);
283
284
		// ID and label are required.
285 10
		if ( ! $args['id'] || ! $args['label'] ) {
286
			return;
287
		}
288
289
		// Total up the cost.
290 10
		$total_cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost'];
291 10
		$taxes      = $args['taxes'];
292
293
		// Taxes - if not an array and not set to false, calc tax based on cost and passed calc_tax variable. This saves shipping methods having to do complex tax calculations.
294 10
		if ( ! is_array( $taxes ) && false !== $taxes && $total_cost > 0 && $this->is_taxable() ) {
295 6
			$taxes = 'per_item' === $args['calc_tax'] ? $this->get_taxes_per_item( $args['cost'] ) : WC_Tax::calc_shipping_tax( $total_cost, WC_Tax::get_shipping_tax_rates() );
296
		}
297
298
		// Round the total cost after taxes have been calculated.
299 10
		$total_cost = wc_format_decimal( $total_cost, $args['price_decimals'] );
300
301
		// Create rate object.
302 10
		$rate = new WC_Shipping_Rate();
303 10
		$rate->set_id( $args['id'] );
304 10
		$rate->set_method_id( $this->id );
305 10
		$rate->set_instance_id( $this->instance_id );
306 10
		$rate->set_label( $args['label'] );
307 10
		$rate->set_cost( $total_cost );
0 ignored issues
show
It seems like $total_cost defined by wc_format_decimal($total...args['price_decimals']) on line 299 can also be of type array<integer,string>; however, WC_Shipping_Rate::set_cost() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
308 10
		$rate->set_taxes( $taxes );
309
310 10
		if ( ! empty( $args['meta_data'] ) ) {
311
			foreach ( $args['meta_data'] as $key => $value ) {
312
				$rate->add_meta_data( $key, $value );
313
			}
314
		}
315
316
		// Store package data.
317 10
		if ( $args['package'] ) {
318 10
			$items_in_package = array();
319 10
			foreach ( $args['package']['contents'] as $item ) {
320 10
				$product            = $item['data'];
321 10
				$items_in_package[] = $product->get_name() . ' &times; ' . $item['quantity'];
322
			}
323 10
			$rate->add_meta_data( __( 'Items', 'woocommerce' ), implode( ', ', $items_in_package ) );
324
		}
325
326 10
		$this->rates[ $args['id'] ] = apply_filters( 'woocommerce_shipping_method_add_rate', $rate, $args, $this );
327
	}
328
329
	/**
330
	 * Calc taxes per item being shipping in costs array.
331
	 *
332
	 * @since 2.6.0
333
	 * @param  array $costs Costs.
334
	 * @return array of taxes
335
	 */
336
	protected function get_taxes_per_item( $costs ) {
337
		$taxes = array();
338
339
		// If we have an array of costs we can look up each items tax class and add tax accordingly.
340
		if ( is_array( $costs ) ) {
341
342
			$cart = WC()->cart->get_cart();
343
344
			foreach ( $costs as $cost_key => $amount ) {
345
				if ( ! isset( $cart[ $cost_key ] ) ) {
346
					continue;
347
				}
348
349
				$item_taxes = WC_Tax::calc_shipping_tax( $amount, WC_Tax::get_shipping_tax_rates( $cart[ $cost_key ]['data']->get_tax_class() ) );
350
351
				// Sum the item taxes.
352 View Code Duplication
				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
353
					$taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 );
354
				}
355
			}
356
357
			// Add any cost for the order - order costs are in the key 'order'.
358
			if ( isset( $costs['order'] ) ) {
359
				$item_taxes = WC_Tax::calc_shipping_tax( $costs['order'], WC_Tax::get_shipping_tax_rates() );
360
361
				// Sum the item taxes.
362 View Code Duplication
				foreach ( array_keys( $taxes + $item_taxes ) as $key ) {
363
					$taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 );
364
				}
365
			}
366
		}
367
368
		return $taxes;
369
	}
370
371
	/**
372
	 * Is this method available?
373
	 *
374
	 * @param array $package Package.
375
	 * @return bool
376
	 */
377 10
	public function is_available( $package ) {
378 10
		$available = $this->is_enabled();
379
380
		// Country availability (legacy, for non-zone based methods).
381 10
		if ( ! $this->instance_id && $available ) {
382 10
			$countries = is_array( $this->countries ) ? $this->countries : array();
0 ignored issues
show
Deprecated Code introduced by
The property WC_Shipping_Method::$countries has been deprecated with message: 2.6.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
383
384 10
			switch ( $this->availability ) {
0 ignored issues
show
Deprecated Code introduced by
The property WC_Shipping_Method::$availability has been deprecated with message: 2.6.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
385
				case 'specific':
386 View Code Duplication
				case 'including':
387
					$available = in_array( $package['destination']['country'], array_intersect( $countries, array_keys( WC()->countries->get_shipping_countries() ) ) );
388
					break;
389 View Code Duplication
				case 'excluding':
390
					$available = in_array( $package['destination']['country'], array_diff( array_keys( WC()->countries->get_shipping_countries() ), $countries ) );
391
					break;
392 View Code Duplication
				default:
393 10
					$available = in_array( $package['destination']['country'], array_keys( WC()->countries->get_shipping_countries() ) );
394 10
					break;
395
			}
396
		}
397
398 10
		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $available, $package, $this );
399
	}
400
401
	/**
402
	 * Get fee to add to shipping cost.
403
	 *
404
	 * @param string|float $fee Fee.
405
	 * @param float        $total Total.
406
	 * @return float
407
	 */
408
	public function get_fee( $fee, $total ) {
409
		if ( strstr( $fee, '%' ) ) {
410
			$fee = ( $total / 100 ) * str_replace( '%', '', $fee );
411
		}
412
		if ( ! empty( $this->minimum_fee ) && $this->minimum_fee > $fee ) {
413
			$fee = $this->minimum_fee;
414
		}
415
		return $fee;
416
	}
417
418
	/**
419
	 * Does this method have a settings page?
420
	 *
421
	 * @return bool
422
	 */
423 10
	public function has_settings() {
424 10
		return $this->instance_id ? $this->supports( 'instance-settings' ) : $this->supports( 'settings' );
425
	}
426
427
	/**
428
	 * Return admin options as a html string.
429
	 *
430
	 * @return string
431
	 */
432 10
	public function get_admin_options_html() {
433 10
		if ( $this->instance_id ) {
434 10
			$settings_html = $this->generate_settings_html( $this->get_instance_form_fields(), false );
435
		} else {
436
			$settings_html = $this->generate_settings_html( $this->get_form_fields(), false );
437
		}
438
439 10
		return '<table class="form-table">' . $settings_html . '</table>';
440
	}
441
442
	/**
443
	 * Output the shipping settings screen.
444
	 */
445
	public function admin_options() {
446
		if ( ! $this->instance_id ) {
447
			echo '<h2>' . esc_html( $this->get_method_title() ) . '</h2>';
448
		}
449
		echo wp_kses_post( wpautop( $this->get_method_description() ) );
450
		echo $this->get_admin_options_html(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped
451
	}
452
453
	/**
454
	 * Get_option function.
455
	 *
456
	 * Gets and option from the settings API, using defaults if necessary to prevent undefined notices.
457
	 *
458
	 * @param  string $key Key.
459
	 * @param  mixed  $empty_value Empty value.
460
	 * @return mixed  The value specified for the option or a default value for the option.
461
	 */
462 504
	public function get_option( $key, $empty_value = null ) {
463
		// Instance options take priority over global options.
464 504
		if ( $this->instance_id && array_key_exists( $key, $this->get_instance_form_fields() ) ) {
465 11
			return $this->get_instance_option( $key, $empty_value );
466
		}
467
468
		// Return global option.
469 504
		$option = apply_filters( 'woocommerce_shipping_' . $this->id . '_option', parent::get_option( $key, $empty_value ), $key, $this );
470 504
		return $option;
471
	}
472
473
	/**
474
	 * Gets an option from the settings API, using defaults if necessary to prevent undefined notices.
475
	 *
476
	 * @param  string $key Key.
477
	 * @param  mixed  $empty_value Empty value.
478
	 * @return mixed  The value specified for the option or a default value for the option.
479
	 */
480 11
	public function get_instance_option( $key, $empty_value = null ) {
481 11
		if ( empty( $this->instance_settings ) ) {
482 11
			$this->init_instance_settings();
483
		}
484
485
		// Get option default if unset.
486 11
		if ( ! isset( $this->instance_settings[ $key ] ) ) {
487
			$form_fields                     = $this->get_instance_form_fields();
488
			$this->instance_settings[ $key ] = $this->get_field_default( $form_fields[ $key ] );
489
		}
490
491 11
		if ( ! is_null( $empty_value ) && '' === $this->instance_settings[ $key ] ) {
492
			$this->instance_settings[ $key ] = $empty_value;
493
		}
494
495 11
		$instance_option = apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_option', $this->instance_settings[ $key ], $key, $this );
496 11
		return $instance_option;
497
	}
498
499
	/**
500
	 * Get settings fields for instances of this shipping method (within zones).
501
	 * Should be overridden by shipping methods to add options.
502
	 *
503
	 * @since 2.6.0
504
	 * @return array
505
	 */
506 11
	public function get_instance_form_fields() {
507 11
		return apply_filters( 'woocommerce_shipping_instance_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->instance_form_fields ) );
508
	}
509
510
	/**
511
	 * Return the name of the option in the WP DB.
512
	 *
513
	 * @since 2.6.0
514
	 * @return string
515
	 */
516 11
	public function get_instance_option_key() {
517 11
		return $this->instance_id ? $this->plugin_id . $this->id . '_' . $this->instance_id . '_settings' : '';
518
	}
519
520
	/**
521
	 * Initialise Settings for instances.
522
	 *
523
	 * @since 2.6.0
524
	 */
525 11 View Code Duplication
	public function init_instance_settings() {
0 ignored issues
show
This method seems to be duplicated in 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...
526 11
		$this->instance_settings = get_option( $this->get_instance_option_key(), null );
527
528
		// If there are no settings defined, use defaults.
529 11
		if ( ! is_array( $this->instance_settings ) ) {
530 11
			$form_fields             = $this->get_instance_form_fields();
531 11
			$this->instance_settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) );
532
		}
533
	}
534
535
	/**
536
	 * Processes and saves global shipping method options in the admin area.
537
	 *
538
	 * This method is usually attached to woocommerce_update_options_x hooks.
539
	 *
540
	 * @since 2.6.0
541
	 * @return bool was anything saved?
542
	 */
543
	public function process_admin_options() {
544
		if ( ! $this->instance_id ) {
545
			return parent::process_admin_options();
546
		}
547
548
		// Check we are processing the correct form for this instance.
549
		if ( ! isset( $_REQUEST['instance_id'] ) || absint( $_REQUEST['instance_id'] ) !== $this->instance_id ) { // WPCS: input var ok, CSRF ok.
550
			return false;
551
		}
552
553
		$this->init_instance_settings();
554
555
		$post_data = $this->get_post_data();
556
557 View Code Duplication
		foreach ( $this->get_instance_form_fields() as $key => $field ) {
558
			if ( 'title' !== $this->get_field_type( $field ) ) {
559
				try {
560
					$this->instance_settings[ $key ] = $this->get_field_value( $key, $field, $post_data );
561
				} catch ( Exception $e ) {
562
					$this->add_error( $e->getMessage() );
563
				}
564
			}
565
		}
566
567
		return update_option( $this->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_settings_values', $this->instance_settings, $this ), 'yes' );
568
	}
569
}
570