Completed
Push — master ( 1ca9d8...e20565 )
by Mike
08:40
created

WC_Widget_Layered_Nav::form()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 1
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
		global $_chosen_attributes;
114
115
		if ( ! is_post_type_archive( 'product' ) && ! is_tax( get_object_taxonomies( 'product' ) ) ) {
116
			return;
117
		}
118
119
		$taxonomy     = isset( $instance['attribute'] ) ? wc_attribute_taxonomy_name( $instance['attribute'] ) : $this->settings['attribute']['std'];
120
		$query_type   = isset( $instance['query_type'] ) ? $instance['query_type'] : $this->settings['query_type']['std'];
121
		$display_type = isset( $instance['display_type'] ) ? $instance['display_type'] : $this->settings['display_type']['std'];
122
123
		if ( ! taxonomy_exists( $taxonomy ) ) {
124
			return;
125
		}
126
127
		$get_terms_args = array( 'hide_empty' => '1' );
128
129
		$orderby = wc_attribute_orderby( $taxonomy );
130
131 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...
132
			case 'name' :
133
				$get_terms_args['orderby']    = 'name';
134
				$get_terms_args['menu_order'] = false;
135
			break;
136
			case 'id' :
137
				$get_terms_args['orderby']    = 'id';
138
				$get_terms_args['order']      = 'ASC';
139
				$get_terms_args['menu_order'] = false;
140
			break;
141
			case 'menu_order' :
142
				$get_terms_args['menu_order'] = 'ASC';
143
			break;
144
		}
145
146
		$terms = get_terms( $taxonomy, $get_terms_args );
147
148
		if ( 0 === sizeof( $terms ) ) {
149
			return;
150
		}
151
152
		ob_start();
153
154
		$this->widget_start( $args, $instance );
155
156
		if ( 'dropdown' === $display_type ) {
157
			$found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type );
158
		} else {
159
			$found = $this->layered_nav_list( $terms, $taxonomy, $query_type );
160
		}
161
162
		$this->widget_end( $args );
163
164
		// Force found when option is selected - do not force found on taxonomy attributes
165
		if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) {
166
			$found = true;
167
		}
168
169
		if ( ! $found ) {
170
			ob_end_clean();
171
		} else {
172
			echo ob_get_clean();
173
		}
174
	}
175
176
	/**
177
	 * Return the currently viewed taxonomy name.
178
	 * @return string
179
	 */
180
	protected function get_current_taxonomy() {
181
		return is_tax() ? get_queried_object()->taxonomy : '';
182
	}
183
184
	/**
185
	 * Return the currently viewed term ID.
186
	 * @return int
187
	 */
188
	protected function get_current_term_id() {
189
		return absint( is_tax() ? get_queried_object()->term_id : 0 );
190
	}
191
192
	/**
193
	 * Return the currently viewed term slug.
194
	 * @return int
195
	 */
196
	protected function get_current_term_slug() {
197
		return absint( is_tax() ? get_queried_object()->slug : 0 );
198
	}
199
200
	/**
201
	 * Show dropdown layered nav.
202
	 * @param  array $terms
203
	 * @param  string $taxonomy
204
	 * @param  string $query_type
205
	 * @return bool Will nav display?
206
	 */
207
	protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) {
208
		global $_chosen_attributes;
209
210
		$found = false;
211
212
		if ( $taxonomy !== $this->get_current_taxonomy() ) {
213
			$taxonomy_filter_name = str_replace( 'pa_', '', $taxonomy );
214
215
			echo '<select class="dropdown_layered_nav_' . esc_attr( $taxonomy_filter_name ) . '">';
216
			echo '<option value="">' . sprintf( __( 'Any %s', 'woocommerce' ), wc_attribute_label( $taxonomy ) ) . '</option>';
217
218
			foreach ( $terms as $term ) {
219
220
				// If on a term page, skip that term in widget list
221
				if ( $term->term_id === $this->get_current_term_id() ) {
222
					continue;
223
				}
224
225
				// Get count based on current view
226
				$_products_in_term = wc_get_term_product_ids( $term->term_id, $taxonomy );
227
				$current_values    = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();
228
				$option_is_set     = in_array( $term->slug, $current_values );
229
230
				// If this is an AND query, only show options with count > 0
231
				if ( 'and' === $query_type ) {
232
					$count = sizeof( array_intersect( $_products_in_term, WC()->query->filtered_product_ids ) );
233
234
					if ( 0 < $count ) {
235
						$found = true;
236
					}
237
238
					if ( 0 === $count && ! $option_is_set ) {
239
						continue;
240
					}
241
242
				// If this is an OR query, show all options so search can be expanded
243
				} else {
244
					$count = sizeof( array_intersect( $_products_in_term, WC()->query->unfiltered_product_ids ) );
245
246
					if ( 0 < $count ) {
247
						$found = true;
248
					}
249
				}
250
251
				echo '<option value="' . esc_attr( $term->slug ) . '" ' . selected( $option_is_set, true, false ) . '>' . esc_html( $term->name ) . '</option>';
252
			}
253
254
			echo '</select>';
255
256
			wc_enqueue_js( "
257
				jQuery( '.dropdown_layered_nav_". esc_js( $taxonomy_filter_name ) . "' ).change( function() {
258
					var slug = jQuery( this ).val();
259
					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;
260
				});
261
			" );
262
		}
263
264
		return $found;
265
	}
266
267
	/**
268
	 * Get current page URL for layered nav items.
269
	 * @return string
270
	 */
271
	protected function get_page_base_url() {
272
		if ( defined( 'SHOP_IS_ON_FRONT' ) ) {
273
			$link = home_url();
274
		} elseif ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id('shop') ) ) {
275
			$link = get_post_type_archive_link( 'product' );
276
		} else {
277
			$link = get_term_link( get_query_var('term'), get_query_var('taxonomy') );
278
		}
279
280
		// Min/Max
281
		if ( isset( $_GET['min_price'] ) ) {
282
			$link = add_query_arg( 'min_price', wc_clean( $_GET['min_price'] ), $link );
283
		}
284
285
		if ( isset( $_GET['max_price'] ) ) {
286
			$link = add_query_arg( 'max_price', wc_clean( $_GET['max_price'] ), $link );
287
		}
288
289
		// Orderby
290 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...
291
			$link = add_query_arg( 'orderby', wc_clean( $_GET['orderby'] ), $link );
292
		}
293
294
		// Search Arg
295
		if ( get_search_query() ) {
296
			$link = add_query_arg( 's', get_search_query(), $link );
297
		}
298
299
		// Post Type Arg
300 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...
301
			$link = add_query_arg( 'post_type', wc_clean( $_GET['post_type'] ), $link );
302
		}
303
304
		return $link;
305
	}
306
307
	/**
308
	 * Show list based layered nav.
309
	 * @param  array $terms
310
	 * @param  string $taxonomy
311
	 * @param  string $query_type
312
	 * @return bool Will nav display?
313
	 */
314
	protected function layered_nav_list( $terms, $taxonomy, $query_type ) {
315
		global $_chosen_attributes;
316
317
		// List display
318
		echo '<ul>';
319
320
		// flip the filtered_products_ids array so that we can use the more efficient array_intersect_key
321
		$filtered_product_ids   = array_flip( WC()->query->filtered_product_ids );
322
		$unfiltered_product_ids = array_flip( WC()->query->unfiltered_product_ids );
323
		$found                  = false;
324
325
		foreach ( $terms as $term ) {
326
			// Get count based on current view - uses transients
327
			// flip the product_in_term array so that we can use array_intersect_key
328
			$_products_in_term = array_flip( wc_get_term_product_ids( $term->term_id, $taxonomy ) );
329
			$current_values    = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array();
330
			$option_is_set     = in_array( $term->slug, $current_values );
331
332
			// skip the term for the current archive
333
			if ( $this->get_current_term_id() === $term->term_id ) {
334
				continue;
335
			}
336
337
			// If this is an AND query, only show options with count > 0
338
			if ( 'and' === $query_type ) {
339
				// Intersect both arrays now they have been flipped so that we can use their keys
340
				$count = sizeof( array_intersect_key( $_products_in_term, $filtered_product_ids ) );
341
342
				if ( 0 < $count ) {
343
					$found = true;
344
				}
345
346
				if ( 0 === $count && ! $option_is_set ) {
347
					continue;
348
				}
349
350
			// If this is an OR query, show all options so search can be expanded
351
			} else {
352
				// Intersect both arrays now they have been flipped so that we can use their keys
353
				$count = sizeof( array_intersect_key( $_products_in_term, $unfiltered_product_ids ) );
354
355
				if ( 0 < $count ) {
356
					$found = true;
357
				}
358
			}
359
360
			$filter_name    = 'filter_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) );
361
			$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( $_GET[ $filter_name ] ) ) : array();
362
			$current_filter = array_map( 'sanitize_title', $current_filter );
363
364
			if ( ! in_array( $term->slug, $current_filter ) ) {
365
				$current_filter[] = $term->slug;
366
			}
367
368
			$link = $this->get_page_base_url();
369
370
			// Add current filters to URL.
371
			foreach ( $current_filter as $key => $value ) {
372
				// Exclude query arg for current term archive term
373
				if ( $value === $this->get_current_term_slug() ) {
374
					unset( $current_filter[ $key ] );
375
				}
376
377
				// Exclude self so filter can be unset on click.
378
				if ( $option_is_set && $value === $term->slug ) {
379
					unset( $current_filter[ $key ] );
380
				}
381
			}
382
383
			if ( ! empty( $current_filter ) ) {
384
				$link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link );
385
386
				// Add Query type Arg to URL
387
				if ( $query_type === 'or' && ! ( 1 === sizeof( $current_filter ) && $option_is_set ) ) {
388
					$link = add_query_arg( 'query_type_' . sanitize_title( str_replace( 'pa_', '', $taxonomy ) ), 'or', $link );
389
				}
390
			}
391
392
			echo '<li class="wc-layered-nav-term ' . ( $option_is_set ? 'chosen' : '' ) . '">';
393
394
			echo ( $count > 0 || $option_is_set ) ? '<a href="' . esc_url( apply_filters( 'woocommerce_layered_nav_link', $link ) ) . '">' : '<span>';
395
396
			echo esc_html( $term->name );
397
398
			echo ( $count > 0 || $option_is_set ) ? '</a>' : '</span>';
399
400
			echo ' <span class="count">(' . absint( $count ) . ')</span></li>';
401
		}
402
403
		echo '</ul>';
404
405
		return $found;
406
	}
407
}
408