Completed
Push — master ( e1ec01...3ab6a2 )
by Mike
08:34
created

WC_Shipping_Zone   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 454
rs 5.7894
wmc 65
lcom 1
cbo 5

28 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 15 5
A get_id() 0 3 1
A create() 0 8 1
A read() 0 10 2
A update() 0 7 1
A delete() 0 10 2
A get_zone_id() 0 3 1
A get_zone_name() 0 3 1
A get_zone_order() 0 3 1
A get_zone_locations() 0 3 1
A generate_zone_name() 0 9 2
A save() 0 19 3
C get_formatted_location() 0 40 7
D get_shipping_methods() 0 40 10
A location_is_continent() 0 3 1
A location_is_country() 0 3 1
A location_is_state() 0 3 1
A location_is_postcode() 0 3 1
A set_zone_id() 0 3 1
A set_zone_name() 0 3 1
A set_zone_order() 0 3 1
A is_valid_location_type() 0 3 1
A add_location() 0 13 3
A clear_locations() 0 11 4
A set_locations() 0 9 2
A read_zone_locations() 0 10 3
A save_locations() 0 15 4
B add_shipping_method() 0 33 3

How to fix   Complexity   

Complex Class

Complex classes like WC_Shipping_Zone often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Shipping_Zone, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit;
5
}
6
7
/**
8
 * Represents a single shipping zone
9
 *
10
 * @class 		WC_Shipping_Zone
11
 * @version		2.6.0
12
 * @package		WooCommerce/Classes
13
 * @category	Class
14
 * @author 		WooThemes
15
 */
16
class WC_Shipping_Zone extends WC_Data {
17
18
	/**
19
	 * Zone Data
20
	 * @var array
21
	 */
22
	protected $_data = array(
23
		'zone_id'        => 0,
24
		'zone_name'      => '',
25
		'zone_order'     => 0,
26
		'zone_locations' => array()
27
	);
28
29
	/**
30
	 * True when location data needs to be re-saved
31
	 * @var bool
32
	 */
33
	private $_locations_changed = false;
34
35
	/**
36
	 * Constructor for zones
37
	 * @param int|object $zone Zone ID to load from the DB (optional) or already queried data.
38
	 */
39
	public function __construct( $zone = 0 ) {
40
		if ( is_numeric( $zone ) && ! empty( $zone ) ) {
41
			$this->read( $zone );
42
		} elseif ( is_object( $zone ) ) {
43
			$this->set_zone_id( $zone->zone_id );
44
			$this->set_zone_name( $zone->zone_name );
45
			$this->set_zone_order( $zone->zone_order );
46
			$this->read_zone_locations( $zone->zone_id );
47
		} elseif ( 0 === $zone ) {
48
			$this->set_zone_name( __( 'Rest of the World', 'woocommerce' ) );
49
			$this->read_zone_locations( 0 );
50
		} else {
51
			$this->set_zone_name( __( 'Zone', 'woocommerce' ) );
52
		}
53
	}
54
55
	/**
56
	 * Get ID
57
	 * @return int
58
	 */
59
	public function get_id() {
60
		return $this->get_zone_id();
61
	}
62
63
	/**
64
	 * Insert zone into the database
65
	 */
66
	public function create() {
67
		global $wpdb;
68
		$wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zones', array(
69
			'zone_name'  => $this->get_zone_name(),
70
			'zone_order' => $this->get_zone_order(),
71
		) );
72
		$this->set_zone_id( $wpdb->insert_id );
73
	}
74
75
	/**
76
	 * Read zone.
77
	 * @param int ID to read from DB
78
	 */
79
	public function read( $id ) {
80
		global $wpdb;
81
82
		if ( $zone_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1;", $id ) ) ) {
83
			$this->set_zone_id( $zone_data->zone_id );
84
			$this->set_zone_name( $zone_data->zone_name );
85
			$this->set_zone_order( $zone_data->zone_order );
86
			$this->read_zone_locations( $zone_data->zone_id );
87
		}
88
	}
89
90
	/**
91
	 * Update zone in the database
92
	 */
93
	public function update() {
94
		global $wpdb;
95
		$wpdb->update( $wpdb->prefix . 'woocommerce_shipping_zones', array(
96
			'zone_name'  => $this->get_zone_name(),
97
			'zone_order' => $this->get_zone_order(),
98
		), array( 'zone_id' => $this->get_zone_id() ) );
99
	}
100
101
	/**
102
	 * Delete a zone.
103
	 * @since 2.6.0
104
	 */
105
	public function delete() {
106
		if ( $this->get_id() ) {
107
			global $wpdb;
108
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'zone_id' => $this->get_id() ) );
109
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $this->get_id() ) );
110
			$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $this->get_id() ) );
111
			WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' );
112
			$this->set_zone_id( 0 );
113
		}
114
	}
115
116
	/**
117
	 * Save zone data to the database.
118
	 */
119
	public function save() {
120
		$name = $this->get_zone_name();
121
122
		if ( empty( $name ) ) {
123
			$this->set_zone_name( $this->generate_zone_name() );
124
		}
125
126
		if ( ! $this->get_zone_id() ) {
127
			$this->create();
128
		} else {
129
			$this->update();
130
		}
131
132
		$this->save_locations();
133
		WC_Cache_Helper::incr_cache_prefix( 'shipping_zones' );
134
135
		// Increments the transient version to invalidate cache.
136
		WC_Cache_Helper::get_transient_version( 'shipping', true );
137
	}
138
139
	/**
140
	 * Get zone ID
141
	 * @return int
142
	 */
143
	public function get_zone_id() {
144
		return absint( $this->_data['zone_id'] );
145
	}
146
147
	/**
148
	 * Get zone name
149
	 * @return string
150
	 */
151
	public function get_zone_name() {
152
		return $this->_data['zone_name'];
153
	}
154
155
	/**
156
	 * Get zone order
157
	 * @return int
158
	 */
159
	public function get_zone_order() {
160
		return absint( $this->_data['zone_order'] );
161
	}
162
163
	/**
164
	 * Get zone locations
165
	 * @return array of zone objects
166
	 */
167
	public function get_zone_locations() {
168
		return $this->_data['zone_locations'];
169
	}
170
171
	/**
172
	 * Generate a zone name based on location.
173
	 * @return string
174
	 */
175
	protected function generate_zone_name() {
176
		$zone_name = $this->get_formatted_location();
177
178
		if ( empty( $zone_name ) ) {
179
			$zone_name = __( 'Zone', 'woocommerce' );
180
		}
181
182
		return $zone_name;
183
	}
184
185
	/**
186
	 * Return a text string representing what this zone is for.
187
	 * @return string
188
	 */
189
	public function get_formatted_location( $max = 10 ) {
190
		$location_parts = array();
191
		$all_continents = WC()->countries->get_continents();
192
		$all_countries  = WC()->countries->get_countries();
193
		$all_states     = WC()->countries->get_states();
194
		$locations      = $this->get_zone_locations();
195
		$continents     = array_filter( $locations, array( $this, 'location_is_continent' ) );
196
		$countries      = array_filter( $locations, array( $this, 'location_is_country' ) );
197
		$states         = array_filter( $locations, array( $this, 'location_is_state' ) );
198
		$postcodes      = array_filter( $locations, array( $this, 'location_is_postcode' ) );
199
200
		foreach ( $continents as $location ) {
201
			$location_parts[] = $all_continents[ $location->code ]['name'];
202
		}
203
204
		foreach ( $countries as $location ) {
205
			$location_parts[] = $all_countries[ $location->code ];
206
		}
207
208
		foreach ( $states as $location ) {
209
			$location_codes = explode( ':', $location->code );
210
			$location_parts[] = $all_states[ $location_codes[ 0 ] ][ $location_codes[ 1 ] ];
211
		}
212
213
		foreach ( $postcodes as $location ) {
214
			$location_parts[] = $location->code;
215
		}
216
217
		// Fix display of encoded characters.
218
		$location_parts = array_map( 'html_entity_decode', $location_parts );
219
220
		if ( sizeof( $location_parts ) > $max ) {
221
			$remaining = sizeof( $location_parts ) - $max;
222
			return sprintf( _n( '%s and %d other region', '%s and %d other regions', $remaining, 'woocommerce' ), implode( ', ', array_splice( $location_parts, 0, $max ) ), $remaining );
223
		} elseif ( ! empty( $location_parts ) ) {
224
			return implode( ', ', $location_parts );
225
		} else {
226
			return __( 'Everywhere', 'woocommerce' );
227
		}
228
	}
229
230
	/**
231
	 * Get shipping methods linked to this zone
232
	 * @param bool Only return enabled methods.
233
	 * @return array of objects
234
	 */
235
	public function get_shipping_methods( $enabled_only = false ) {
236
		global $wpdb;
237
238
		$raw_methods_sql = $enabled_only ? "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1 order by method_order ASC;" : "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d order by method_order ASC;";
239
		$raw_methods     = $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $this->get_zone_id() ) );
240
		$wc_shipping     = WC_Shipping::instance();
241
		$allowed_classes = $wc_shipping->get_shipping_method_class_names();
242
		$methods         = array();
243
244
		foreach ( $raw_methods as $raw_method ) {
245
			if ( in_array( $raw_method->method_id, array_keys( $allowed_classes ) ) ) {
246
				$class_name = $allowed_classes[ $raw_method->method_id ];
247
248
				// The returned array may contain instances of shipping methods, as well
249
				// as classes. If the "class" is an instance, just use it. If not,
250
				// create an instance.
251
				if ( is_object( $class_name ) ) {
252
					$class_name_of_instance = get_class( $class_name );
253
					$methods[ $raw_method->instance_id ] = new $class_name_of_instance( $raw_method->instance_id );
254
				} else {
255
					// If the class is not an object, it should be a string. It's better
256
					// to double check, to be sure (a class must be a string, anything)
257
					// else would be useless
258
					if ( is_string( $class_name ) && class_exists( $class_name ) ) {
259
						$methods[ $raw_method->instance_id ] = new $class_name( $raw_method->instance_id );
260
					}
261
				}
262
263
				// Let's make sure that we have an instance before setting its attributes
264
				if ( is_object( $methods[ $raw_method->instance_id ] ) ) {
265
					$methods[ $raw_method->instance_id ]->method_order  = absint( $raw_method->method_order );
266
					$methods[ $raw_method->instance_id ]->enabled       = $raw_method->is_enabled ? 'yes' : 'no';
267
					$methods[ $raw_method->instance_id ]->has_settings  = $methods[ $raw_method->instance_id ]->has_settings();
268
					$methods[ $raw_method->instance_id ]->settings_html = $methods[ $raw_method->instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $raw_method->instance_id ]->get_admin_options_html() : false;
269
				}
270
			}
271
		}
272
273
		return apply_filters( 'woocommerce_shipping_zone_shipping_methods', $methods, $raw_methods, $allowed_classes, $this );
274
	}
275
276
	/**
277
	 * Location type detection
278
	 * @param  object  $location
279
	 * @return boolean
280
	 */
281
	private function location_is_continent( $location ) {
282
		return 'continent' === $location->type;
283
	}
284
285
	/**
286
	 * Location type detection
287
	 * @param  object  $location
288
	 * @return boolean
289
	 */
290
	private function location_is_country( $location ) {
291
		return 'country' === $location->type;
292
	}
293
294
	/**
295
	 * Location type detection
296
	 * @param  object  $location
297
	 * @return boolean
298
	 */
299
	private function location_is_state( $location ) {
300
		return 'state' === $location->type;
301
	}
302
303
	/**
304
	 * Location type detection
305
	 * @param  object  $location
306
	 * @return boolean
307
	 */
308
	private function location_is_postcode( $location ) {
309
		return 'postcode' === $location->type;
310
	}
311
312
	/**
313
	 * Set zone ID
314
	 * @access private
315
	 * @param int $set
316
	 */
317
	private function set_zone_id( $set ) {
318
		$this->_data['zone_id'] = absint( $set );
319
	}
320
321
	/**
322
	 * Set zone name
323
	 * @param string $set
324
	 */
325
	public function set_zone_name( $set ) {
326
		$this->_data['zone_name'] = wc_clean( $set );
327
	}
328
329
	/**
330
	 * Set zone order
331
	 * @param int $set
332
	 */
333
	public function set_zone_order( $set ) {
334
		$this->_data['zone_order'] = absint( $set );
335
	}
336
337
	/**
338
	 * Is passed location type valid?
339
	 * @param  string  $type
340
	 * @return boolean
341
	 */
342
	public function is_valid_location_type( $type ) {
343
		return in_array( $type, array( 'postcode', 'state', 'country', 'continent' ) );
344
	}
345
346
	/**
347
	 * Add location (state or postcode) to a zone.
348
	 * @param string $code
349
	 * @param string $type state or postcode
350
	 */
351
	public function add_location( $code, $type ) {
352
		if ( $this->is_valid_location_type( $type ) ) {
353
			if ( 'postcode' === $type ) {
354
				$code = trim( strtoupper( $code ) ); // No normalization - postcodes are matched against both normal and formatted versions to support wildcards.
355
			}
356
			$location = array(
357
				'code' => wc_clean( $code ),
358
				'type' => wc_clean( $type )
359
			);
360
			$this->_data['zone_locations'][] = (object) $location;
361
			$this->_locations_changed = true;
362
		}
363
	}
364
365
	/**
366
	 * Clear all locations for this zone.
367
	 * @param array|string $types of location to clear
368
	 */
369
	public function clear_locations( $types = array( 'postcode', 'state', 'country', 'continent' ) ) {
370
		if ( ! is_array( $types ) ) {
371
			$types = array( $types );
372
		}
373
		foreach ( $this->_data['zone_locations'] as $key => $values ) {
374
			if ( in_array( $values->type, $types ) ) {
375
				unset( $this->_data['zone_locations'][ $key ] );
376
				$this->_locations_changed = true;
377
			}
378
		}
379
	}
380
381
	/**
382
	 * Set locations
383
	 * @param array $locations Array of locations
384
	 */
385
	public function set_locations( $locations = array() ) {
386
		$this->clear_locations();
387
388
		foreach ( $locations as $location ) {
389
			$this->add_location( $location['code'], $location['type'] );
390
		}
391
392
		$this->_locations_changed = true;
393
	}
394
395
	/**
396
	 * Read location data from the database
397
	 * @param  int $zone_id
398
	 */
399
	private function read_zone_locations( $zone_id ) {
400
		global $wpdb;
401
402
		if ( $locations = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d;", $zone_id ) ) ) {
403
			foreach ( $locations as $location ) {
404
				$this->add_location( $location->location_code, $location->location_type );
405
			}
406
		}
407
		$this->_locations_changed = false;
408
	}
409
410
	/**
411
	 * Save locations to the DB.
412
	 *
413
	 * This function clears old locations, then re-inserts new if any changes are found.
414
	 */
415
	private function save_locations() {
416
		if ( ! $this->get_zone_id() || ! $this->_locations_changed ) {
417
			return false;
418
		}
419
		global $wpdb;
420
		$wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $this->get_zone_id() ) );
421
422
		foreach ( $this->get_zone_locations() as $location ) {
423
			$wpdb->insert( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array(
424
				'zone_id'       => $this->get_zone_id(),
425
				'location_code' => $location->code,
426
				'location_type' => $location->type
427
			) );
428
		}
429
	}
430
431
	/**
432
	 * Add a shipping method to this zone.
433
	 * @param string $type shipping method type
434
	 * @return int new instance_id, 0 on failure
435
	 */
436
	public function add_shipping_method( $type ) {
437
		global $wpdb;
438
439
		$instance_id     = 0;
440
		$wc_shipping     = WC_Shipping::instance();
441
		$allowed_classes = $wc_shipping->get_shipping_method_class_names();
442
		$count           = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $this->get_zone_id() ) );
443
444
		if ( in_array( $type, array_keys( $allowed_classes ) ) ) {
445
			$wpdb->insert(
446
				$wpdb->prefix . 'woocommerce_shipping_zone_methods',
447
				array(
448
					'method_id'    => $type,
449
					'zone_id'      => $this->get_zone_id(),
450
					'method_order' => ( $count + 1 )
451
				),
452
				array(
453
					'%s',
454
					'%d',
455
					'%d'
456
				)
457
			);
458
			$instance_id = $wpdb->insert_id;
459
		}
460
461
		if ( $instance_id ) {
462
			do_action( 'woocommerce_shipping_zone_method_added', $instance_id, $type, $this->get_zone_id() );
463
		}
464
465
		WC_Cache_Helper::get_transient_version( 'shipping', true );
466
467
		return $instance_id;
468
	}
469
}
470