Skip to content

Instantly share code, notes, and snippets.

@rhaglennydd
Last active July 10, 2020 16:44
Show Gist options
  • Save rhaglennydd/b7a3ed4102811f613ecb614e3b7082c7 to your computer and use it in GitHub Desktop.
Save rhaglennydd/b7a3ed4102811f613ecb614e3b7082c7 to your computer and use it in GitHub Desktop.
A (mostly) working WooCommerce solution to ensure certain attribute pulldown option combinations are impossible

A client wants it so that it's not possible for customers to select product attribute pulldown combinations that refer to either non-existent or out of stock variations (they don't like customers making a selection from four pulldowns only to be told it's invalid). I took a stab at coding a solution that mostly works.

From functions.php the class SingleProduct is instantiated and the setup method is called to kick things off. The JavaScript is in the productVariationFooterScript method. Every time the user selects an option from an attribute pulldown an AJAX request is made, which is handled by the method ajaxFilterProductAttributeOptions (it returns valid options for all the other pulldowns, which then get updated by JavaScript). Like I say it mostly works, the attribute pulldown options are filtered properly for the most part, the only hitch I've found is that if the user makes selections from the first three pulldowns sometimes they can hit a combination for which any of the options for the last pulldown results in an out of stock variation. (There are variations like this in the system.) Thus no options are returned for that pulldown via AJAX, so its options are unchanged. I suspect this code needs to be reworked substantially to be more intelligent, maybe involving recursion, but I'm not sure how I would go about it.

<?php
class SingleProduct implements DependencyInterface
{
private const ATTRIBUTE_PREFIX = 'attribute_';
/**
* @param array $product_variation
* @param array $filter_criteria
* @param array $product_atts
* @param array $filtered_options
* @param array $options_order
*/
private function filterProductVariationOptions(
array $product_variation,
array $filter_criteria,
array &$product_atts,
array &$filtered_options,
array &$options_order
) {
$att_prefix_len = strlen(self::ATTRIBUTE_PREFIX);
extract($product_variation);
/**
* @var array $attributes
* @var bool $is_in_stock
* @var bool $variation_is_active
* @var bool $variation_is_visible
*/
$variation_atts = $attributes;
unset($attributes);
if (
! ($variation_is_active && $variation_is_visible && $is_in_stock) ||
! $variation_atts
) {
return;
}
$matched_all = true;
foreach ((array)$filter_criteria as $att_key => $att_val) {
if ($att_val !== ($variation_atts[ $att_key ] ?? '')) {
$matched_all = false;
break;
}
}
if (! $matched_all) {
return;
}
foreach ((array)$variation_atts as $att_key => $att_val) {
$att_name = substr($att_key, $att_prefix_len);
if (
! (empty($filter_criteria[ $att_key ]) &&
isset($product_atts[ $att_name ]))
) {
continue;
}
if (isset($filtered_options[ $att_key ])) {
$filtered_options_values =
wp_list_pluck($filtered_options[ $att_key ], 'value');
if (in_array($att_val, $filtered_options_values)) {
continue;
}
}
/** @var WC_Product_Attribute $product_att */
$product_att = &$product_atts[ $att_name ];
if ($product_att->is_taxonomy()) {
$taxonomy = $product_att->get_taxonomy();
$term = get_term_by('slug', $att_val, $taxonomy);
if ($term instanceof WP_Term) {
/** @var WP_Term $term */
$filtered_options[ $att_key ][] = [
'value' => $term->slug,
'name' => $term->name
];
}
} else {
$filtered_options[ $att_key ][] = [
'value' => $att_val,
'name' => $att_val
];
}
if (! isset($options_order[ $att_key ])) {
$product_att_options = $product_att->get_options();
$options_order[ $att_key ] = [
'attribute' => &$product_att,
'options' => $product_att_options
];
}
}
}
/**
* @param WC_Product|WP_Post|int|bool $product
* @param string $att_name
*
* @return WC_Product_Attribute|null
*/
private function getProductAttribute(
$product,
string $att_name
): ?WC_Product_Attribute {
$product = ($product instanceof WC_Product) ? $product :
wc_get_product($product);
if (! $product instanceof WC_Product) {
return null;
}
$att_prefix_len = strlen(self::ATTRIBUTE_PREFIX);
if (self::ATTRIBUTE_PREFIX === substr($att_name, 0, $att_prefix_len)) {
$att_name = substr($att_name, $att_prefix_len);
}
$atts = $product->get_attributes();
return $atts[ $att_name ] ?? null;
}
/**
* @param array $options
* @param array $options_sorted
* @param string $taxonomy
*/
private function sortProductAttributeOptions(
array &$options,
array $options_sorted,
string $taxonomy
) {
usort(
$options,
function ($a, $b) use ($options_sorted, $taxonomy) {
$return = 0;
if ($taxonomy) {
$a = get_term_by('slug', $a, $taxonomy);
$b = get_term_by('slug', $b, $taxonomy);
if (($a instanceof WP_Term) && ($b instanceof WP_Term)) {
/**
* @var WP_Term $a
* @var WP_Term $b
*/
$a = $a->term_id;
$b = $b->term_id;
} else {
$a = $b = null;
}
}
$a_index = array_search($a, $options_sorted);
$b_index = array_search($b, $options_sorted);
if (! in_array(false, [$a_index, $b_index], true)) {
$return = $a_index <=> $b_index;
}
return $return;
}
);
}
public function ajaxFilterProductAttributeOptions(): void
{
$filtered_options = $options_order = $product_atts = [];
$input_vars = wp_parse_args($_POST, ['product_id' => 0]);
$product_id = absint($input_vars[ 'product_id' ]);
$product = wc_get_product($product_id);
$att_prefix = 'attribute_';
$att_prefix_len = strlen($att_prefix);
$input_atts = [];
foreach ((array)$input_vars as $att_key => $val) {
if (substr($att_key, 0, $att_prefix_len) === $att_prefix) {
$input_atts[ $att_key ] = $val;
}
}
if ($product instanceof WC_Product_Variable) {
$variations = $product->get_available_variations();
$product_atts = $product->get_attributes();
foreach ((array)$variations as $variation) {
$this->filterProductVariationOptions(
$variation,
$input_atts,
$product_atts,
$filtered_options,
$options_order
);
}
};
foreach ((array)$input_vars as $att_key => $value) {
if (substr($att_key, 0, $att_prefix_len) === $att_prefix) {
$product_att = $this->getProductAttribute($product, $att_key);
if (! $product_att) {
continue;
}
$name = $value;
if ($product_att->is_taxonomy()) {
$taxonomy = $product_att->get_taxonomy();
$term = get_term_by('slug', $value, $taxonomy);
if ($term instanceof WP_Term) {
$name = $term->name;
}
}
$selected = true;
$filtered_options[ $att_key ][] =
compact('name', 'selected', 'value');
}
}
foreach (array_keys($filtered_options) as $att_key) {
if (! isset($options_order[ $att_key ])) {
continue;
}
/** @var WC_Product_Attribute $product_att */
$product_att = $options_order[ $att_key ][ 'attribute' ];
$options_sorted = $options_order[ $att_key ][ 'options' ];
$taxonomy = $product_att->get_taxonomy();
$this->sortProductAttributeOptions(
$filtered_options[ $att_key ],
$options_sorted,
$taxonomy
);
}
die(json_encode(['options' => $filtered_options]));
}
public function ajaxResetProductAttributeOptions(): void
{
$input_vars = wp_parse_args($_POST, ['product_id' => 0]);
$product_id = absint($input_vars[ 'product_id' ]);
$product = wc_get_product($product_id);
$options = [];
if ($product instanceof WC_Product_Variable) {
/**
* @var WC_Product_Variable $product
* @var WC_Product_Attribute[] $atts
*/
$atts = $product->get_attributes();
foreach ((array)$atts as $att_name => $att) {
/** @var WC_Product_Attribute $att */
$att_key = self::ATTRIBUTE_PREFIX . $att_name;
if ($att->is_taxonomy()) {
$terms = $att->get_terms();
foreach ($terms as $term) {
/** @var WP_Term $term */
$options[ $att_key ][] = [
'value' => $term->slug,
'name' => $term->name
];
}
} else {
$att_options = $att->get_options();
foreach ($att_options as $att_option) {
$options[ $att_key ][] = [
'value' => $att_option,
'name' => $att_option
];
}
}
}
}
die(json_encode(compact('options')));
}
/**
* @param WP $wp
*/
public function mainQueryReady(WP $wp)
{
if (! is_singular('product')) {
return;
}
$product = wc_get_product();
if ($product instanceof WC_Product_Variable) {
add_action(
'wp_print_footer_scripts',
[$this, 'productVariationFooterScript']
);
}
unset($wp);
}
public function productVariationFooterScript(): void
{
?>
<!--suppress JSNonStrictModeUsed -->
<script>
(function($) {
'use strict';
var ajax_url = <?= json_encode(admin_url('admin-ajax.php')) ?>,
$variations_form = $(".variations_form"),
$reset_button = $variations_form.find("a.reset_variations"),
$selectors = $variations_form.find(".variations select"),
product_id = $variations_form.data("product_id");
/**
*
* @param {Object} options
* @param {String|JQuery} selectors
*/
function repopulate_options(options, selectors) {
var $selectors = $(selectors);
$selectors.each(function() {
var $selector = $(this),
att_name = $selector.data("attribute_name"),
att_options;
if ('undefined' === typeof options[ att_name ]) {
return true;
}
att_options = options[ att_name ];
$selector.find("option[value!='']").remove();
$.each(att_options, function(key, option) {
var $option = $("<option><" + "/option>").attr(
"value", option.value
).text(option.name);
if ('undefined' !== typeof option.selected) {
$option.prop('selected', !!option.selected);
}
$selector.append($option);
});
return true;
});
}
$selectors.on("change", function() {
var post_data = {
action : "filter_product_attribute_options",
product_id : product_id
},
values_selected = false;
$selectors.each(function() {
var $this = $(this),
att_name = $this.data("attribute_name"),
val = $this.val();
if ('' !== val) {
values_selected = true;
post_data[ att_name ] = val;
}
});
if (values_selected) {
$.post(ajax_url, post_data, function(response) {
var options = response.options;
repopulate_options(options, $selectors);
}, "json");
}
});
$reset_button.on("click", function() {
var post_data = {
action : "reset_product_attribute_options",
product_id : product_id
};
$.post(ajax_url, post_data, function(response) {
var options = response.options;
repopulate_options(options, $selectors);
}, "json");
});
})(jQuery);
</script>
<?php }
public function setup(): void
{
if (wp_doing_ajax()) {
add_action(
'wp_ajax_nopriv_filter_product_attribute_options',
[$this, 'ajaxFilterProductAttributeOptions']
);
add_action(
'wp_ajax_filter_product_attribute_options',
[$this, 'ajaxFilterProductAttributeOptions']
);
add_action(
'wp_ajax_nopriv_reset_product_attribute_options',
[$this, 'ajaxResetProductAttributeOptions']
);
add_action(
'wp_ajax_reset_product_attribute_options',
[$this, 'ajaxResetProductAttributeOptions']
);
} elseif (! is_admin()) {
add_action('wp', [$this, 'mainQueryReady']);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment