Woocommerce: and the mysterious Product Attributes

Kevin Ruscoe
3 min readJan 16, 2021

Woocommerce handles it’s Product Attributes weirdly, mainly because they make use of WordPress’s taxonomies and terms. I’m currently writing a script to import Shopify products into WooCommerce, and it’s going fine. But the biggest problem I strumbled across was the product attributes. The general code looked like this …

$product_attributes = [
'Size' => 'Small',
'Colour' => 'Red',
'Rating' => '5 Star!'
];
$product = new WC_Product;// Add names, prices, etc.$attributes = [];foreach ($product_attributes as $name => $value) {
$attribute = new WC_Product_Attribute();
$attribute->set_name($name);
$attribute->set_options($value);
$attributes[] = $attribute;
}
$product->set_attributes($attributes);$product->save();

Pretty straight forward, create a product, loop an array of attributes and save.

This works fine, the product was made and the attributes were added. So I dug into the theme customizer and added WooCommerces “Filter by Attribute” widget to allow the user to filter by “Red” things. To my surprise, I could not select the attributes I added to the product.

The problem

It turns out, after much Googling, that only global attributes (which are taxonomy based) can be filtered with this widget. This half makes sense, I’m sure you could create another widget to collect product attributes, but whatever.

So, a product can have 2 types of attributes; local ones (metafield values), and global ones (taxonomy terms). So, no biggy,we’ll just add these global ones and try again.

$global_attributes = ['Size', 'Colour'];foreach ($global_attributes as $global_attribute) {
wc_create_attribute([
'name' => $global_attribute,
'type' => 'text',
]);
}

This will create the global attributes for us, so let’s run the product importer again, and get these working. Everything looks fine, but heading over to the widget again, the Size and Colour attributes still aren’t there. Great. It turns out WooCommerce isn’t smart enough to map these for us.

The solution

The solution is kind of clunky. You need to get both the taxonomy_id and term_id and set these on the WC_Product_Attribute object to tell WooCommerce that these are taxonomies terms, not product attributes.

When you create a global attribute, WooCommerce slugify the name and prefixes it with pa_ (assuming that stands for product attribute), making the attributes created with wc_create_attribute(); ‘pa_size’ and ‘pa_colour’.

Back in our foreach ($product_attributes as $name => $value) loop, we need to determine if the current attribute is global, we can use in_array() for this. Check the current $name is within the $global_attributes array.

Next, we need to create the term on the correct taxonomy. I’m pretty sure WooCommerce has a helper to correctly get a global attribute name, but $term_name = "pa_" . sanitize_title($key); works fine. To get the actual term (like ‘Red’) use $term = get_term_by('name', $value, $term_name).

This will probably blow up if the current value (i.e. Red) doesn’t exist in the current tax (i.e. Colour), so you’ll need to add it with wp_insert_term($value, $term_name); then fetch the term. My working-but-probably-horribly-inefficient code to create and fetch the term looks like …

if (! $term = get_term_by( 'name', $value, $term_name )) {
wp_insert_term( $value, $term_name );
$term = get_term_by( 'name', $value, $term_name );
}

Now we finally have the actual term object ($term), we need to use the set_id(), set_name() and set_options() methods on the WC_Product_Attribute object during the product creation.

Looking through the WC_Product_Attribute class, it seems that the $id is either 0 with local attributes, or the ID of the taxonomy if it’s global (term_taxonomy_id). This is how WooCommerce links these attributes to taxonomy. It does the same with the option to link the term (term_id).

So, the code …

$attribute->set_id($term->term_taxonomy_id);
$attribute->set_name($term_name);
$attribute->set_options([$term->term_id]);

Finally, they’re linked, so we just save the product and we’re all good.

Final code

// Add global attributes.$global_attributes = ['Size', 'Colour'];foreach ($global_attributes as $global_attribute) {
wc_create_attribute([
'name' => $global_attribute,
'type' => 'text',
]);
}
// Add product.$product_attributes = [
'Size' => 'Small',
'Colour' => 'Red',
'Rating' => '5 Star!'
];
$product = new WC_Product;// Add names, prices, etc.$attributes = [];foreach ($product_attributes as $name => $value) {
$attribute = new WC_Product_Attribute();
$attribute->set_name($name);
$attribute->set_options($value);
if (in_array($key, $global_attributes)) {
// Deal with global attributes.
$term_name = "pa_" . sanitize_title( $name );
if (! $term = get_term_by( 'name', $value, $term_name )) {
wp_insert_term( $value, $term_name );
$term = get_term_by( 'name', $value, $term_name );
}
$attribute->set_id($term->term_taxonomy_id);
$attribute->set_name($term_name);
$attribute->set_options([$term->term_id]);
}
$attributes[] = $attribute;}$product->set_attributes($attributes);$product->save();

Now we can update the widget and add the filters option.

--

--