Completed
Pull Request — master (#10315)
by Mike
08:27
created

WC_Widget_Layered_Nav   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 418
Duplicated Lines 12.2 %

Coupling/Cohesion

Components 1
Dependencies 2
Metric Value
wmc 82
lcom 1
cbo 2
dl 51
loc 418
rs 4.8717

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A update() 0 4 1
A form() 0 4 1
B init_settings() 0 44 4
D widget() 14 62 16
A get_current_taxonomy() 0 3 2
A get_current_term_id() 0 3 2
A get_current_term_slug() 0 3 2
D layered_nav_dropdown() 5 45 10
D get_page_base_url() 27 56 15
B get_filtered_term_product_counts() 0 33 4
F layered_nav_list() 5 72 24

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Widget_Layered_Nav 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_Widget_Layered_Nav, and based on these observations, apply Extract Interface, too.

1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 16 and the first side effect is on line 4.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit;
5
}
6
7
/**
8
 * Layered Navigation Widget.
9
 *
10
 * @author   WooThemes
11
 * @category Widgets
12
 * @package  WooCommerce/Widgets
13
 * @version  2.6.0
14
 * @extends  WC_Widget
15
 */
16
class WC_Widget_Layered_Nav extends WC_Widget {
17
18
	/**
19
	 * Constructor.
20
	 */
21
	public function __construct() {
22
		$this->widget_cssclass    = 'woocommerce widget_layered_nav';
23
		$this->widget_description = __( 'Shows a custom attribute in a widget which lets you narrow down the list of products when viewing product categories.', 'woocommerce' );
24
		$this->widget_id          = 'woocommerce_layered_nav';
25
		$this->widget_name        = __( 'WooCommerce Layered Nav', 'woocommerce' );
26
		parent::__construct();
27
	}
28
29
	/**
30
	 * Updates a particular instance of a widget.
31
	 *
32
	 * @see WP_Widget->update
33
	 *
34
	 * @param array $new_instance
35
	 * @param array $old_instance
36
	 *
37
	 * @return array
38
	 */
39
	public function update( $new_instance, $old_instance ) {
40
		$this->init_settings();
41
		return parent::update( $new_instance, $old_instance );
42
	}
43
44
	/**
45
	 * Outputs the settings update form.
46
	 *
47
	 * @see WP_Widget->form
48
	 *
49
	 * @param array $instance
50
	 */
51
	public function form( $instance ) {
52
		$this->init_settings();
53
		parent::form( $instance );
54
	}
55
56
	/**
57
	 * Init settings after post types are registered.
58
	 */
59
	public function init_settings() {
60
		$attribute_array      = array();
61
		$attribute_taxonomies = wc_get_attribute_taxonomies();
62
63
		if ( $attribute_taxonomies ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $attribute_taxonomies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
64
			foreach ( $attribute_taxonomies as $tax ) {
65
				if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
66
					$attribute_array[ $tax->attribute_name ] = $tax->attribute_name;
67
				}
68
			}
69
		}
70
71
		$this->settings = array(
72
			'title' => array(
73
				'type'  => 'text',
74
				'std'   => __( 'Filter by', 'woocommerce' ),
75
				'label' => __( 'Title', 'woocommerce' )
76
			),
77
			'attribute' => array(
78
				'type'    => 'select',
79
				'std'     => '',
80
				'label'   => __( 'Attribute', 'woocommerce' ),
81
				'options' => $attribute_array
82
			),
83
			'display_type' => array(
84
				'type'    => 'select',
85
				'std'     => 'list',
86
				'label'   => __( 'Display type', 'woocommerce' ),
87
				'options' => array(
88
					'list'     => __( 'List', 'woocommerce' ),
89
					'dropdown' => __( 'Dropdown', 'woocommerce' )
90
				)
91
			),
92
			'query_type' => array(
93
				'type'    => 'select',
94
				'std'     => 'and',
95
				'label'   => __( 'Query type', 'woocommerce' ),
96
				'options' => array(
97
					'and' => __( 'AND', 'woocommerce' ),
98
					'or'  => __( 'OR', 'woocommerce' )
99
				)
100
			),
101
		);
102
	}
103
104
	/**
105
	 * Output widget.
106
	 *
107
	 * @see WP_Widget
108
	 *
109
	 * @param array $args
110
	 * @param array $instance
111
	 */
112
	public function widget( $args, $instance ) {
113
		if ( ! is_post_type_archive( 'product' ) && ! is_tax( get_object_taxonomies( 'product' ) ) ) {
114
			return;
115
		}
116
117
		$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
118
		$taxonomy           = isset( $instance['attribute'] ) ? wc_attribute_taxonomy_name( $instance['attribute'] ) : $this->settings['attribute']['std'];
119
		$query_type         = isset( $instance['query_type'] ) ? $instance['query_type'] : $this->settings['query_type']['std'];
120
		$display_type       = isset( $instance['display_type'] ) ? $instance['display_type'] : $this->settings['display_type']['std'];
121
122
		if ( ! taxonomy_exists( $taxonomy ) ) {
123
			return;
124
		}
125
126
		$get_terms_args = array( 'hide_empty' => '1' );
127
128
		$orderby = wc_attribute_orderby( $taxonomy );
129
130 View Code Duplication
		switch ( $orderby ) {
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...
131
			case 'name' :
132
				$get_terms_args['orderby']    = 'name';
133
				$get_terms_args['menu_order'] = false;
134
			break;
135
			case 'id' :
136
				$get_terms_args['orderby']    = 'id';
137
				$get_terms_args['order']      = 'ASC';
138
				$get_terms_args['menu_order'] = false;
139
			break;
140
			case 'menu_order' :
141
				$get_terms_args['menu_order'] = 'ASC';
142
			break;
143
		}
144
145
		$terms = get_terms( $taxonomy, $get_terms_args );
146
147
		if ( 0 === sizeof( $terms ) ) {
148
			return;
149
		}
150
151
		ob_start();
152
153
		$this->widget_start( $args, $instance );
154
155
		if ( 'dropdown' === $display_type ) {
156
			$found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type );
157
		} else {
158
			$found = $this->layered_nav_list( $terms, $taxonomy, $query_type );
159
		}
160
161
		$this->widget_end( $args );
162
163
		// Force found when option is selected - do not force found on taxonomy attributes
164
		if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) {
165
			$found = true;
166
		}
167
168
		if ( ! $found ) {
169
			ob_end_clean();
170
		} else {
171
			echo ob_get_clean();
172
		}
173
	}
174
175
	/**
176
	 * Return the currently viewed taxonomy name.
177
	 * @return string
178
	 */
179
	protected function get_current_taxonomy() {
180
		return is_tax() ? get_queried_object()->taxonomy : '';
181
	}
182
183
	/**
184
	 * Return the currently viewed term ID.
185
	 * @return int
186
	 */
187
	protected function get_current_term_id() {
188
		return absint( is_tax() ? get_queried_object()->term_id : 0 );
189
	}
190
191
	/**
192
	 * Return the currently viewed term slug.
193
	 * @return int
194
	 */
195
	protected function get_current_term_slug() {
196
		return absint( is_tax() ? get_queried_object()->slug : 0 );
197
	}
198
199
	/**
200
	 * Show dropdown layered nav.
201
	 * @param  array $terms
202
	 * @param  string $taxonomy
203
	 * @param  string $query_type
204
	 * @return bool Will nav display?
205
	 */
206
	protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) {
207
		$found = false;
208
209
		if ( $taxonomy !== $this->get_current_taxonomy() ) {
210
			$term_counts          = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type );
211
			$_chosen_attributes   = WC_Query::get_layered_nav_chosen_attributes();
212
			$taxonomy_filter_name = str_replace( 'pa_', '', $taxonomy );
213
214
			echo '<select class="dropdown_layered_nav_' . esc_attr( $taxonomy_filter_name ) . '">';
215
			echo '<option value="">' . sprintf( __( 'Any %s', 'woocommerce' ), wc_attribute_label( $taxonomy ) ) . '</option>';
216
217
			foreach ( $terms as $term ) {
218
219
				// If on a term page, skip that term in widget list
220
				if ( $term->term_id === $this->get_current_term_id() ) {
221
					continue;
222
				}
223
224
				// Get count based on current view
225
				$current_values    = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();
226
				$option_is_set     = in_array( $term->slug, $current_values );
227
				$count             = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;
228
229
				// Only show options with count > 0
230 View Code Duplication
				if ( 0 < $count ) {
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...
231
					$found = true;
232
				} elseif ( 'and' === $query_type && 0 === $count && ! $option_is_set ) {
233
					continue;
234
				}
235
236
				echo '<option value="' . esc_attr( $term->slug ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( $term->name ) . '</option>';
237
			}
238
239
			echo '</select>';
240
241
			wc_enqueue_js( "
242
				jQuery( '.dropdown_layered_nav_". esc_js( $taxonomy_filter_name ) . "' ).change( function() {
243
					var slug = jQuery( this ).val();
244
					location.href = '" . preg_replace( '%\/page\/[0-9]+%', '', str_replace( array( '&amp;', '%2C' ), array( '&', ',' ), esc_js( add_query_arg( 'filtering', '1', remove_query_arg( array( 'page', 'filter_' . $taxonomy_filter_name ) ) ) ) ) ) . "&filter_". esc_js( $taxonomy_filter_name ) . "=' + slug;
245
				});
246
			" );
247
		}
248
249
		return $found;
250
	}
251
252
	/**
253
	 * Get current page URL for layered nav items.
254
	 * @return string
255
	 */
256
	protected function get_page_base_url( $taxonomy ) {
257 View Code Duplication
		if ( defined( 'SHOP_IS_ON_FRONT' ) ) {
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...
258
			$link = home_url();
259
		} elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id('shop') ) ) {
260
			$link = get_post_type_archive_link( 'product' );
261
		} else {
262
			$link = get_term_link( get_query_var('term'), get_query_var('taxonomy') );
263
		}
264
265
		// Min/Max
266
		if ( isset( $_GET['min_price'] ) ) {
267
			$link = add_query_arg( 'min_price', wc_clean( $_GET['min_price'] ), $link );
268
		}
269
270
		if ( isset( $_GET['max_price'] ) ) {
271
			$link = add_query_arg( 'max_price', wc_clean( $_GET['max_price'] ), $link );
272
		}
273
274
		// Orderby
275 View Code Duplication
		if ( isset( $_GET['orderby'] ) ) {
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...
276
			$link = add_query_arg( 'orderby', wc_clean( $_GET['orderby'] ), $link );
277
		}
278
279
		// Search Arg
280
		if ( get_search_query() ) {
281
			$link = add_query_arg( 's', get_search_query(), $link );
282
		}
283
284
		// Post Type Arg
285 View Code Duplication
		if ( isset( $_GET['post_type'] ) ) {
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...
286
			$link = add_query_arg( 'post_type', wc_clean( $_GET['post_type'] ), $link );
287
		}
288
289
		// Min Rating Arg
290
		if ( isset( $_GET['min_rating'] ) ) {
291
			$link = add_query_arg( 'min_rating', wc_clean( $_GET['min_rating'] ), $link );
292
		}
293
294
		// All current filters
295 View Code Duplication
		if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) {
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...
296
			foreach ( $_chosen_attributes as $name => $data ) {
297
				if ( $name === $taxonomy ) {
298
					continue;
299
				}
300
				$filter_name = sanitize_title( str_replace( 'pa_', '', $name ) );
301
				if ( ! empty( $data['terms'] ) ) {
302
					$link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link );
303
				}
304
				if ( 'or' == $data['query_type'] ) {
305
					$link = add_query_arg( 'query_type_' . $filter_name, 'or', $link );
306
				}
307
			}
308
		}
309
310
		return $link;
311
	}
312
313
	/**
314
	 * Count products within certain terms, taking the main WP query into consideration.
315
	 * @param  array $term_ids
316
	 * @param  string $taxonomy
317
	 * @param  string $query_type
318
	 * @return array
319
	 */
320
	protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
321
		global $wpdb;
322
323
		$tax_query  = WC_Query::get_main_tax_query();
324
		$meta_query = WC_Query::get_main_meta_query();
325
326
		if ( 'or' === $query_type ) {
327
			foreach ( $tax_query as $key => $query ) {
328
				if ( $taxonomy === $query['taxonomy'] ) {
329
					unset( $tax_query[ $key ] );
330
				}
331
			}
332
		}
333
334
		$meta_query     = new WP_Meta_Query( $meta_query );
335
		$tax_query      = new WP_Tax_Query( $tax_query );
336
		$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
337
		$tax_query_sql  = $tax_query->get_sql( $wpdb->posts, 'ID' );
338
339
		$sql  = "
340
			SELECT COUNT( {$wpdb->posts}.ID ) as term_count, term_count_relationships.term_taxonomy_id as term_count_id FROM {$wpdb->posts}
341
			INNER JOIN {$wpdb->term_relationships} AS term_count_relationships ON ({$wpdb->posts}.ID = term_count_relationships.object_id)
342
			" . $tax_query_sql['join'] . $meta_query_sql['join'] . "
343
			WHERE {$wpdb->posts}.post_type = 'product' AND {$wpdb->posts}.post_status = 'publish'
344
			" . $tax_query_sql['where'] . $meta_query_sql['where'] . "
345
			AND term_count_relationships.term_taxonomy_id IN (" . implode( ',', array_map( 'absint', $term_ids ) ) . ")
346
			GROUP BY term_count_relationships.term_taxonomy_id;
347
		";
348
349
		$results = $wpdb->get_results( $sql );
350
351
		return wp_list_pluck( $results, 'term_count', 'term_count_id' );
352
	}
353
354
	/**
355
	 * Show list based layered nav.
356
	 * @param  array $terms
357
	 * @param  string $taxonomy
358
	 * @param  string $query_type
359
	 * @return bool Will nav display?
360
	 */
361
	protected function layered_nav_list( $terms, $taxonomy, $query_type ) {
362
		// List display
363
		echo '<ul>';
364
365
		$term_counts        = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type );
366
		$_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes();
367
		$found              = false;
368
369
		foreach ( $terms as $term ) {
370
			$current_values    = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();
371
			$option_is_set     = in_array( $term->slug, $current_values );
372
			$count             = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0;
373
374
			// skip the term for the current archive
375
			if ( $this->get_current_term_id() === $term->term_id ) {
376
				continue;
377
			}
378
379
			// Only show options with count > 0
380 View Code Duplication
			if ( 0 < $count ) {
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...
381
				$found = true;
382
			} elseif ( 'and' === $query_type && 0 === $count && ! $option_is_set ) {
383
				continue;
384
			}
385
386
			$filter_name    = 'filter_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) );
387
			$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( $_GET[ $filter_name ] ) ) : array();
388
			$current_filter = array_map( 'sanitize_title', $current_filter );
389
390
			if ( ! in_array( $term->slug, $current_filter ) ) {
391
				$current_filter[] = $term->slug;
392
			}
393
394
			$link = $this->get_page_base_url( $taxonomy );
395
396
			// Add current filters to URL.
397
			foreach ( $current_filter as $key => $value ) {
398
				// Exclude query arg for current term archive term
399
				if ( $value === $this->get_current_term_slug() ) {
400
					unset( $current_filter[ $key ] );
401
				}
402
403
				// Exclude self so filter can be unset on click.
404
				if ( $option_is_set && $value === $term->slug ) {
405
					unset( $current_filter[ $key ] );
406
				}
407
			}
408
409
			if ( ! empty( $current_filter ) ) {
410
				$link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link );
411
412
				// Add Query type Arg to URL
413
				if ( $query_type === 'or' && ! ( 1 === sizeof( $current_filter ) && $option_is_set ) ) {
414
					$link = add_query_arg( 'query_type_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) ), 'or', $link );
415
				}
416
			}
417
418
			echo '<li class="wc-layered-nav-term ' . ( $option_is_set ? 'chosen' : '' ) . '">';
419
420
			echo ( $count > 0 || $option_is_set ) ? '<a href="' . esc_url( apply_filters( 'woocommerce_layered_nav_link', $link ) ) . '">' : '<span>';
421
422
			echo esc_html( $term->name );
423
424
			echo ( $count > 0 || $option_is_set ) ? '</a>' : '</span>';
425
426
			echo ' <span class="count">(' . absint( $count ) . ')</span></li>';
427
		}
428
429
		echo '</ul>';
430
431
		return $found;
432
	}
433
}
434