WooCommerce Layered Nav Widget With Metadata
TutorialsWooCommerce 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');
}