WooCommerce Layered Nav Widget With Metadata

WooCommerce Layered Nav Widget With Metadata

Tutorials

WooCommerce contains a Layered Nav Widget which enables customers to filter products on shop pages through custom product attributes. The code below originates from the WooCommerce Layered Nav Widget, and filters products based on: a custom product meta called _clearance, WooCommerce’s _instock product meta, and the product’s post_date.

In the WooCommerce Layered Nav Widget settings, there is an option to change the query type to and/or. This code uses only the and query type.

UPDATE: See Filter by Meta Widget for WooCommerce for WooCommerce 2.6.0+ compatibility.

<?php
/**
 * Plugin Name: Custom Nav Filters
 * Description: Shows custom product metadata in a widget which lets you narrow down the list of products when viewing product categories.
 * Author: Matt Messick
 * Version: 1.0
 * Author URI: http://mattmessick.com
 */

if (! defined('ABSPATH')) {
	exit;
}

if (in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {

	/**
	 * Class Custom_Nav_Filters_Widget
	 */
	class Custom_Nav_Filters_Widget extends WP_Widget
	{
		/**
		 * Custom_Nav_Filters_Widget constructor.
		 */
		public function __construct()
		{
			parent::__construct('custom_nav_filters', 'Custom Nav Filters', [
				'description' => 'Shows custom product metadata in a widget which lets you narrow down the list of products when viewing product categories.'
			]);
		}

		/**
		 * Outputs the settings update form.
		 *
		 * @param array $instance
		 *
		 * @return string|void
		 */
		public function form($instance)
		{
			$defaults = ['title' => 'Filter', 'clearance' => 'off', 'in_stock' => 'off', 'new_arrivals' => 'off'];
			$instance = wp_parse_args((array) $instance, $defaults); ?>

            <p>
                <label for="<?php echo $this->get_field_id('title'); ?>"><?php _e('Title'); ?></label>
                <input class="widefat" type="text" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($instance['title']); ?>"/>
            </p>
            <p>
                <input class="checkbox" type="checkbox" <?php checked($instance['clearance'], 'on'); ?> id="<?php echo $this->get_field_id('clearance'); ?>" name="<?php echo $this->get_field_name('clearance'); ?>"/>
                <label for="<?php echo $this->get_field_id('clearance'); ?>"> <?php _e('Clearance'); ?></label>
            </p>
            <p>
                <input class="checkbox" type="checkbox" <?php checked($instance['in_stock'], 'on'); ?> id="<?php echo $this->get_field_id('in_stock'); ?>" name="<?php echo $this->get_field_name('in_stock'); ?>"/>
                <label for="<?php echo $this->get_field_id('in_stock'); ?>"><?php _e('In Stock'); ?></label>
            </p>
            <p>
                <input class="checkbox" type="checkbox" <?php checked($instance['new_arrivals'], 'on'); ?> id="<?php echo $this->get_field_id('new_arrivals'); ?>" name="<?php echo $this->get_field_name('new_arrivals'); ?>"/>
                <label for="<?php echo $this->get_field_id('new_arrivals'); ?>"><?php _e('New Arrivals'); ?></label>
            </p>
		<?php }

		/**
		 * Updates a particular instance of a widget.
		 *
		 * @param array $new_instance
		 * @param array $old_instance
		 *
		 * @return array
		 */
		public function update($new_instance, $old_instance)
		{
			$instance = $old_instance;
			$instance['title'] = strip_tags($new_instance['title']);
			$instance['clearance'] = $new_instance['clearance'];
			$instance['in_stock'] = $new_instance['in_stock'];
			$instance['new_arrivals'] = $new_instance['new_arrivals'];

			return $instance;
		}

		/**
		 * Echoes the widget content.
		 *
		 * @param array $args
		 * @param array $instance
		 */
		public function widget($args, $instance)
		{
			/**
			 * @var $before_title
			 * @var $after_title
			 * @var $before_widget
			 * @var $after_widget
			 */
			global $_selected_filters, $wpdb;

			$checkboxes = [];

			extract($args);

			if (! is_post_type_archive('product') && ! is_tax(get_object_taxonomies('product'))) {
				return;
			}

			//Add active meta filters to $checkboxes
			if ($instance['clearance'] == 'on') {
				$checkboxes[] = ['id' => 'clearance', 'title' => 'Clearance'];
			}

			if ($instance['in_stock'] == 'on') {
				$checkboxes[] = ['id' => 'in_stock', 'title' => 'In Stock'];
			}

			if ($instance['new_arrivals'] == 'on') {
				$checkboxes[] = ['id' => 'new_arrivals', 'title' => 'New Arrivals'];
			}

			if (! empty($checkboxes)) {

				//Add filter query argument to WC Layered Nav Widget & WC Price Filter Widget
				if (isset($_GET['filter']) && ! empty($_GET['filter'])) {
					wc_enqueue_js("
						function add_query_arg(uri, key, value) {
							var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i');
							var separator = uri.indexOf('?') !== -1 ? '&' : '?';
							if (uri.match(re)) {
								return uri.replace(re, '$1' + key + '=' + value + '$2');
							} else {
								return uri + separator + key + '=' + value;
							}
						}

						//Woocommerce layered nav widget
						if (jQuery('.widget_layered_nav').length) {
							jQuery('.widget_layered_nav ul li a').each( function() {
								var uri = jQuery(this).attr('href');
								var update_uri = add_query_arg(uri, 'filter', '" . $_GET['filter'] . "');
								jQuery(this).attr('href', update_uri);
							});
						}

						//Woocommerce price filter widget
						if (jQuery('.widget_price_filter').length) {
							jQuery('.widget_price_filter form .price_slider_amount').append('<input type='hidden' name='filter' value='" . $_GET['filter'] . "'>);
						}
					");
				}

				ob_start();

				$found = false;

				$arg = 'filter';

				echo $before_widget;
				$title = empty($instance['title']) ? ' ' : apply_filters('widget_title', $instance['title']);

				if (! empty($title)) {
					echo $before_title . $title . $after_title;
				}

				echo '<ul>';

				//Loop filters
				foreach ($checkboxes as $checkbox) {

					$sql = '';
					$this_filter = $checkbox['id'];

					//Select the product IDs
					switch ($this_filter) {

						//Based on custom product meta: _clearance
						case 'clearance':
							$sql = "SELECT DISTINCT pm.post_id FROM $wpdb->postmeta pm
									JOIN $wpdb->posts p ON (p.ID = pm.post_id)
									WHERE pm.meta_key = '_clearance'
									AND pm.meta_value = 'yes'
									AND p.post_type = 'product'
									AND p.post_status = 'publish'";
							break;

						//Based on Woocommerce product meta: _in_stock
						case 'in_stock':
							$sql = "SELECT DISTINCT pm.post_id FROM $wpdb->postmeta pm
									JOIN $wpdb->posts p ON (p.ID = pm.post_id)
									WHERE pm.meta_key = '_stock_status'
									AND pm.meta_value = 'instock'
									AND p.post_type = 'product'
									AND p.post_status = 'publish'";
							break;

						//Based on date: post_date >= 7 days from now
						case 'new_arrivals':
							$sql = "SELECT DISTINCT pm.post_id FROM $wpdb->postmeta pm
									JOIN $wpdb->posts p ON (p.ID = pm.post_id)
									WHERE p.post_date >= NOW()-INTERVAL 7 DAY
									AND p.post_type = 'product'
									AND p.post_status = 'publish'";
							break;

					}

					//Create transient
					$transient_name = 'cnf_ln_count_' . md5(sanitize_key($arg) . sanitize_key($this_filter));

					//If transient is not set then create new transient with product IDs
					if (false === ($_products_with_meta = get_transient($transient_name))) {

						$_products_with_meta = $wpdb->get_col($sql);
						set_transient($transient_name, $_products_with_meta);
					}

					$option_is_set = (in_array($this_filter, $_selected_filters));

					$count = sizeof(array_intersect($_products_with_meta, WC()->query->filtered_product_ids));

					if ($count > 0) {
						$found = true;
					}

					if ($count == 0 && ! $option_is_set) {
						continue;
					}

					$selected_filters = (isset($_GET[$arg])) ? explode(',', $_GET[$arg]) : [];

					if (! is_array($selected_filters)) {
						$selected_filters = [];
					}

					$selected_filters = array_map('esc_attr', $selected_filters);

					//Combine selected filters with this filter
					if (! in_array($this_filter, $selected_filters)) {
						$selected_filters[] = $this_filter;
					}

					//Base link decided by current page
					if (defined('SHOP_IS_ON_FRONT')) {
						$link = home_url();
					} elseif (is_post_type_archive('product') || is_page(wc_get_page_id('shop'))) {
						$link = get_post_type_archive_link('product');
					} else {
						$link = get_term_link(get_query_var('term'), get_query_var('taxonomy'));
					}

					if ($_selected_filters) {
						foreach ($_selected_filters as $_selected_filter) {

							//Add selected filters to query arg if not this filter
							if (! empty($_selected_filter) && $_selected_filter !== $this_filter) {
								$link = add_query_arg($arg, $_selected_filter, $link);
							}
						}
					}

					//Query qrgs - Min/Max
					if (isset($_GET['min_price'])) {
						$link = add_query_arg('min_price', $_GET['min_price'], $link);
					}

					if (isset($_GET['max_price'])) {
						$link = add_query_arg('max_price', $_GET['max_price'], $link);
					}

					//Query qrgs - Orderby
					if (isset($_GET['orderby'])) {
						$link = add_query_arg('orderby', $_GET['orderby'], $link);
					}

					//Query qrgs - Search Arg
					if (get_search_query()) {
						$link = add_query_arg('s', get_search_query(), $link);
					}

					//Query qrgs - Post Type Arg
					if (isset($_GET['post_type'])) {
						$link = add_query_arg('post_type', $_GET['post_type'], $link);
					}

					//Query qrgs - Attribute Arg
					if (isset($_GET['filter_brand'])) {
						$link = add_query_arg('filter_brand', $_GET['filter_brand'], $link);
					}

					if (isset($_selected_filters) && in_array($this_filter, $_selected_filters)) {

						$class = 'class="selected"';

						//If more than one selected filter then remove this filter from selected filters and add selected filters to query arg
						if (sizeof($selected_filters) > 1) {
							$selected_filters_without_this = array_diff($selected_filters, [$this_filter]);
							$link = add_query_arg($arg, implode(',', $selected_filters_without_this), $link);
						}
					} else {

						$class = '';
						$link = add_query_arg($arg, implode(',', $selected_filters), $link);

					}

					//Output
					echo '<li ' . $class . '>';
					echo ($count > 0) ? '<a href="' . esc_url($link) . '">' : '<span>';
					echo $checkbox['title'];
					echo ($count > 0) ? '</a>' : '</span>';
					echo ' <small class="count">' . $count . '</small></li>';
				}

				echo '</ul>';

				echo $after_widget;

				if (! $found) {
					ob_end_clean();
				} else {
					echo ob_get_clean();
				}
			}
		}
	}

	/**
	 * Register Custom_Nav_Filters_Widget.
	 */
	function custom_nav_filters_init()
	{
		register_widget('Custom_Nav_Filters_Widget');
	}

	add_action('widgets_init', 'custom_nav_filters_init');

	/**
	 * Init Custom_Nav_Filters_Widget.
	 */
	function cnf_nav_init()
	{
		if (is_active_widget(false, false, 'custom_nav_filters', true) && ! is_admin()) {
			global $_selected_filters;

			$_selected_filters = [];

			if (! empty($_GET['filter'])) {
				$_selected_filters = explode(',', $_GET['filter']);
			}

			add_filter('loop_shop_post_in', 'cnf_nav_query');
		}
	}

	add_action('init', 'cnf_nav_init');

	/**
	 * Query filtered posts.
	 *
	 * @param $filtered_posts
	 *
	 * @return array
	 */
	function cnf_nav_query($filtered_posts)
	{
		global $_selected_filters;

		if (sizeof($_selected_filters) > 0) {

			$matched_products_from_filter = [];

			//Loop through selected filters
			foreach ($_selected_filters as $_selected_filter) {
				$date_query = $meta_query = [];
				$filtered = false;

				switch ($_selected_filter) {
					case 'clearance':
						$meta_query[] = [
							'key' => '_clearance',
							'value' => 'yes',
							'meta_compare' => '='
						];
						break;
					case 'in_stock':
						$meta_query[] = [
							'key' => '_stock_status',
							'value' => 'instock',
							'meta_compare' => '='
						];
						break;
					case 'new_arrivals':
						$date_query[] = [
							'after' => '1 week ago',
						];
						break;
				}

				//Retrieve products with selected filter
				$posts = get_posts([
					'post_type' => 'product',
					'numberposts' => -1,
					'post_status' => 'publish',
					'fields' => 'ids',
					'no_found_rows' => true,
					'meta_query' => $meta_query,
					'date_query' => $date_query
				]);

				//Combine filtered results
				if (! is_wp_error($posts)) {

					if (sizeof($matched_products_from_filter) > 0 || $filtered) {
						$matched_products_from_filter = array_intersect($posts, $matched_products_from_filter);
					} else {
						$matched_products_from_filter = $posts;
					}

					$filtered = true;
				}
			}
		}

		//Set results
		if ($filtered) {
			WC()->query->layered_nav_post__in = $matched_products_from_filter;
			WC()->query->layered_nav_post__in[] = 0;

			if (sizeof($filtered_posts) == 0) {
				$filtered_posts = $matched_products_from_filter;
				$filtered_posts[] = 0;
			} else {
				$filtered_posts = array_intersect($filtered_posts, $matched_products_from_filter);
				$filtered_posts[] = 0;
			}

		}

		return (array) $filtered_posts;
	}

	/**
	 * Delete cnf_delete_transient.
	 */
	function cnf_delete_transient()
	{
		global $wpdb;

		$wpdb->query("
			DELETE FROM `$wpdb->options`
			WHERE `option_name` LIKE ('_transient_cnf_ln_count_%')
		");
	}

	add_action('woocommerce_delete_product_transients', 'cnf_delete_transient');
}

Leave a Reply

Your email address will not be published. Required fields are marked *