Managing variable products in WooCommerce—especially with multiple attributes—is often a manual nightmare for eCommerce store owners, you'll quickly find yourself struggling.
Generating and keeping multiple product variations in sync with attribute options in WP Woocommerce is a hell!
- Manually updating product variations every time a new model is released ❌
- Default WooCommerce limitations, such as a max of 50 generated variations ❌
- No automated sync for attributes across products ❌
- Issues with CSV import/export in PHP 8.1 ❌
- Local challenges with WooCommerce REST API auth ❌
Now, imagine the online store selling phone cases with multiple attributes (e.g., brand, color), where each case is a variable product.
👉 Business Problem:
- Each phone case is a variable product.
- The phone models available (
pa_iphone,pa_samsung, etc.) should be auto-set globally - Same for color attribute options (
pa_color) - The default selected brand attribute can be updated based on analytic report
- New phone models creating process is a routine, wasting
- Default WooCommerce limit is 50 variations per product (which is too low).
- Re-generating variations manually is really time-consuming
After some unsuccesfull research gave up and end up with my own solution
⚠️ Be careful! The all code commin above is tested on PHP8.1 and has no backward compatibility
We reach the max variations generation limit of 50 for Apple iPhones (31 * 2) = 62, and it will grow each year.
Add the following to wp-config.php to increase the limit:
define('WC_MAX_LINKED_VARIATIONS', 200);
set_time_limit(3000000);
⚠️ Always start with a backup
Easy db backup with wp-cli example:
wp db export ./db.sql
wp db import ./db.sql # restoreI do recomend making manipulations in local enviorment, then exporting the changed db for production
⚠️ Localhost only the routes should be protected for production enviorment
To ensure proper attribute handling, we create a WooCommerce REST API endpoint (/devices/debug) to fetch product details dynamically.
The public GET endpoint to retrive some important for tests and sync information
Location: /devices/debug
<?php
const SHOP_BRANDS = ['motorola', 'iphone', 'samsung', 'xiaomi'];
const SHOP_PRICE = '599'; // Set default price, adjust as needed
const SHOP_ATTR_NAME_HASHMAP = [
'pa_motorola' => 'Motorola',
'pa_iphone' => 'iPhone',
'pa_samsung' => 'Samsung',
'pa_xiaomi' => 'Xiaomi',
'pa_barva' => 'Barva',
];
// 0. Query all products by brand
/**
* Enables querying in wc_get_products by custom pwb-brand
* @example 'brand' => '[x]' | 'brand' => 'iphone' | 'xiaomi' | 'samsung' | 'motorola'
*/
function shop_woo_query_add_filter__brand(
array $query_args,
array $query_vars
) : array
{
if (!in_array($query_vars['brand'], SHOP_BRANDS)) {
return $query_args;
}
if (!empty($query_vars['brand'])) {
$query_args['tax_query'][] = [
'taxonomy' => 'pwb-brand',
'field' => 'slug',
'terms' => $query_vars['brand']
];
}
return $query_args;
}
add_filter('woocommerce_product_data_store_cpt_get_products_query', 'shop_woo_query_add_filter__brand', 10, 2);
/**
* Query phone cases by brand
* @param string $brand = 'motorola' | 'iphone' | 'samsung' | 'xiaomi'
* @param string[]|string $category = 'kolekce'
*
* @returns int[] array of phone product IDs
*
* @see WC_Product_Query
* */
function shop_query_cases_by_brand(
string $brand = 'motorola' | 'iphone' | 'samsung' | 'xiaomi',
string|array $category = 'kolekce'
): array
{
return array_map(fn(WC_Product $phone) => $phone->get_id(), wc_get_products([
'limit' => -1,
'category' => $category,
'brand' => $brand,
]));
}
/**
* Enables REST API endpoint for querying all required data for testing purposes
* @return array
* @see https://doffcases.test/wp-json/devices/debug
* */
add_action( 'rest_api_init', function () {
register_rest_route( 'devices', '/debug', [
'methods' => WP_REST_Server::READABLE,
'callback' => function (WP_REST_Request $_request) {
$log = ['product_ids' => [], 'product_ids_count' => 0, 'attribute_options' => []];
foreach (SHOP_BRANDS as $brand) {
$brand_product_ids = shop_query_cases_by_brand($brand);
$log['product_ids'] = array_merge($log['product_ids'], $brand_product_ids);
$log[$brand . '_product_ids'] = $brand_product_ids;
$log[$brand . '_product_ids_count'] = count($brand_product_ids);
$log['attribute_options'][$brand] = array_map(fn(WP_Term $term) => [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'count' => $term->count,
], get_terms([
'taxonomy' => 'pa_' . $brand,
'hide_empty' => false,
]));
}
$log['product_ids_count'] = count($log['product_ids']);
return rest_ensure_response($log);
},
]);
});Now visit the route https://doffcases.test/wp-json/devices/debug to check that we retrive all the data correctly
Response representation
Response json representation:
{
"product_ids":[
15173,
14958
// ...
],
"product_ids_count":315,
"attribute_options":{
"motorola":[
{
"id":231,
"name":"Edge 20",
"slug":"edge-20",
"count":2
},
{
"id":232,
"name":"Edge 40",
"slug":"edge-40",
"count":2
}
],
"iphone":[
{
"id":102,
"name":"iPhone 11",
"slug":"iphone-11",
"count":106
},
{
"id":103,
"name":"iPhone 11 Pro",
"slug":"iphone-11-pro",
"count":105
},
{
"id":104,
"name":"iPhone 11 Pro Max",
"slug":"iphone-11-pro-max",
"count":105
},
{
"id":177,
"name":"iPhone 12",
"slug":"iphone-12",
"count":105
},
{
"id":98,
"name":"iPhone 12 mini",
"slug":"iphone-12-mini",
"count":105
},
{
"id":99,
"name":"iPhone 12 Pro",
"slug":"iphone-12-pro",
"count":106
},
{
"id":100,
"name":"iPhone 12 PRO MAX",
"slug":"iphone-12-pro-max",
"count":105
},
{
"id":178,
"name":"iPhone 13",
"slug":"iphone-13",
"count":105
},
{
"id":230,
"name":"iPhone 13 mini",
"slug":"iphone_13mini",
"count":105
},
{
"id":95,
"name":"iPhone 13 mini",
"slug":"iphone-13-mini",
"count":105
},
{
"id":96,
"name":"iPhone 13 PRO",
"slug":"iphone-13-pro",
"count":107
},
{
"id":97,
"name":"iPhone 13 PRO Max",
"slug":"iphone-13-pro-max",
"count":107
},
{
"id":174,
"name":"iPhone 14",
"slug":"iphone-14",
"count":106
},
{
"id":91,
"name":"iPhone 14 Plus",
"slug":"iphone-14-plus",
"count":107
},
{
"id":92,
"name":"iPhone 14 Plus Pro",
"slug":"iphone-14-plus-pro",
"count":105
},
{
"id":93,
"name":"iPhone 14 Plus Pro Max",
"slug":"iphone-14-plus-pro-max",
"count":105
},
{
"id":175,
"name":"iPhone 14 Pro",
"slug":"iphone-14-pro",
"count":108
},
{
"id":210,
"name":"iPhone 14 Pro Max",
"slug":"iphone-14-pro-max",
"count":109
},
{
"id":211,
"name":"iPhone 15",
"slug":"iphone-15",
"count":106
},
{
"id":212,
"name":"iPhone 15 Plus",
"slug":"iphone-15-plus",
"count":106
},
{
"id":213,
"name":"iPhone 15 Pro",
"slug":"iphone-15-pro",
"count":106
},
{
"id":214,
"name":"iPhone 15 Pro Max",
"slug":"iphone-15-pro-max",
"count":108
},
{
"id":246,
"name":"iPhone 16",
"slug":"iphone-16",
"count":105
},
{
"id":243,
"name":"iPhone 16 Plus",
"slug":"iphone-16-plus",
"count":105
},
{
"id":244,
"name":"iPhone 16 Pro",
"slug":"iphone-16-pro",
"count":105
},
{
"id":245,
"name":"iPhone 16 Pro Max",
"slug":"iphone-16-pro-max",
"count":105
},
{
"id":101,
"name":"iPhone SE 2 (2020)",
"slug":"iphone-se",
"count":106
},
{
"id":94,
"name":"iPhone SE 3 (2022)",
"slug":"iphone-se-3",
"count":105
},
{
"id":227,
"name":"iPhone X",
"slug":"iphone-x",
"count":105
},
{
"id":229,
"name":"iPhone XR",
"slug":"iphone-xr",
"count":105
},
{
"id":228,
"name":"iPhone XS Max",
"slug":"iphone-xs-max",
"count":105
}
],
"samsung":[
{
"id":241,
"name":"A 33",
"slug":"a-33",
"count":104
},
{
"id":240,
"name":"A 34",
"slug":"a-34",
"count":104
},
{
"id":239,
"name":"A 52",
"slug":"a-52",
"count":104
},
{
"id":238,
"name":"A 52 S",
"slug":"a-52-s",
"count":104
},
{
"id":237,
"name":"A 53",
"slug":"samsung_a53",
"count":104
},
{
"id":236,
"name":"Galaxy S 20",
"slug":"galaxy-s-20",
"count":104
},
{
"id":233,
"name":"Galaxy S 24",
"slug":"galaxy-s-24",
"count":104
},
{
"id":235,
"name":"Galaxy S 24 Plus",
"slug":"galaxy-s-24-plus",
"count":104
},
{
"id":234,
"name":"Galaxy S 24 Ultra",
"slug":"galaxy-s-24-ultra",
"count":104
},
{
"id":242,
"name":"Galaxy S20 FE",
"slug":"galaxy-s20-fe",
"count":104
},
{
"id":117,
"name":"S 21",
"slug":"s-21",
"count":104
},
{
"id":118,
"name":"S 21 Ultra",
"slug":"s-21-ultra",
"count":104
},
{
"id":176,
"name":"S 22",
"slug":"s-22",
"count":104
},
{
"id":119,
"name":"S 22 Ultra",
"slug":"s-22-ultra",
"count":104
},
{
"id":120,
"name":"S 23",
"slug":"s-23",
"count":104
},
{
"id":121,
"name":"S 23 Ultra",
"slug":"s-23-ultra",
"count":104
}
],
"xiaomi":[
{
"id":224,
"name":"Redmi Note 10",
"slug":"redmi-note-10",
"count":104
},
{
"id":219,
"name":"Xiaomi 11",
"slug":"xiaomi-11",
"count":104
},
{
"id":226,
"name":"Redmi 10",
"slug":"redmi-10",
"count":104
},
{
"id":225,
"name":"Redmi 12",
"slug":"redmi-12",
"count":104
},
{
"id":220,
"name":"Redmi Note 11",
"slug":"redmi-note-11",
"count":104
},
{
"id":223,
"name":"Redmi Note 12 Pro",
"slug":"redmi-note-12-pro",
"count":104
},
{
"id":221,
"name":"Redmi Note 12 S",
"slug":"redmi-note-12-s",
"count":104
},
{
"id":122,
"name":"Xiaomi 12",
"slug":"xiaomi-12",
"count":104
},
{
"id":123,
"name":"Xiaomi 12 Pro",
"slug":"xiaomi-12-pro",
"count":104
},
{
"id":124,
"name":"Xiaomi 13",
"slug":"xiaomi-13",
"count":104
},
{
"id":125,
"name":"Xiaomi 13 Pro",
"slug":"xiaomi-13-pro",
"count":104
}
]
},
"motorola_product_ids":[
15173,
14958
],
"motorola_product_ids_count":2,
"iphone_product_ids":[
11935,
11917
],
"iphone_product_ids_count":105,
"samsung_product_ids":[
13976,
12825
],
"samsung_product_ids_count":104,
"xiaomi_product_ids":[
15166,
13926
],
"xiaomi_product_ids_count":104
}See the woo API documentation on how to update the product variations: woo api docs
Now let's define some utility functions and endpoint to reach functionality:
- Sync product attributes with global taxonomy values
- Setting default attributes
- Generating new product variations
- Set the price for each product variation
The public GET endpoint to update the prucuct
Location: /devices/<product_id>/generate
/**
* Set attribute helper
* @param string $pa_taxonomy_name
* @param array $pa_taxonomy_options
* @return WC_Product_Attribute
*/
function set_attribute_helper(
string $pa_taxonomy_name,
array $pa_taxonomy_options = []
) : WC_Product_Attribute
{
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_name( $pa_taxonomy_name );
// If no options provided, get them programmatically
if (empty($pa_taxonomy_options)) {
$pa_taxonomy_options = array_map(fn (WP_Term $term) => $term->term_id, get_terms([
'taxonomy' => $pa_taxonomy_name,
'hide_empty' => false,
]));
}
// Set options to provided values or get all term ids
$attribute_object->set_options( $pa_taxonomy_options );
$attribute_object->set_visible( true );
$attribute_object->set_variation( true );
// if pa_barva, set position to 1, otherwise 0
$position = 0;
if ($pa_taxonomy_name === 'pa_barva') {
$position = 1;
}
$attribute_object->set_position($position);
$taxonomy_id = wc_attribute_taxonomy_id_by_name( SHOP_ATTR_NAME_HASHMAP[ $pa_taxonomy_name ] );
$attribute_object->set_id( $taxonomy_id );
return $attribute_object;
}
/**
* TODO: protect this endpoint, make it private
* @see https://doffcases.test/wp-json/devices/15173/generate
* */
add_action( 'rest_api_init', function () {
register_rest_route( 'devices', '/(?P<id>\d+)/generate', [
'methods' => WP_REST_Server::READABLE,
'callback' => function ( WP_REST_Request $data ) {
$log = ['ok' => false];
if (!isset($data['id'])) {
$log['res'] = 'Missing product ID in request';
$log['data'] = $data;
return rest_ensure_response($log);
}
$product_id = $data['id'];
/** @var $brand WP_Term[] */
$brands = get_the_terms($product_id, 'pwb-brand'); // 'motorola' | 'iphone' | 'samsung' | 'xiaomi'
if (!$brands) {
$log['res'] = 'Cannot get product brand, set pwb-brand';
return rest_ensure_response($log);
}
$brand = $brands[0]->slug;
if (!in_array($brand, SHOP_BRANDS)) {
$log['res'] = 'Invalid product brand, expected: ' . implode(' | ', SHOP_BRANDS);
return rest_ensure_response($log);
}
/** @var $product WC_Product */
$product = wc_get_product($product_id);
if (!$product) {
$log['res'] = 'Cannot get product by ID';
return rest_ensure_response($log);
}
$log['ok'] = true;
/** @var $attributes WC_Product_Attribute[] */
$attributes = $product->get_attributes();
// Unset color attribute if exists
if (isset($attributes['pa_barva'])) {
unset($attributes['pa_barva']);
}
$color_attribute = set_attribute_helper('pa_barva', [106, 105]);
$attributes['pa_barva'] = $color_attribute;
/** Brand attribute name, like 'pa_motorola' | 'pa_iphone' | 'pa_samsung' | 'pa_xiaomi' */
$brand_attribute_name = 'pa_' . $brand;
// Unset brand attribute if exists
if (isset($attributes[$brand_attribute_name])) {
unset($attributes[$brand_attribute_name]);
}
$brand_attribute = set_attribute_helper($brand_attribute_name);
$attributes[$brand_attribute_name] = $brand_attribute;
// Last element of an array
// $last_el = fn(array $arr) => $arr[count($arr) - 1];
// 'pa_barva' => get_term_by('id', $last_el($color_attribute->get_options()), 'pa_barva')->slug,
$product->set_attributes($attributes);
$product->set_default_attributes([
'pa_' . $brand => [
'iphone' => 'iphone-16',
'motorola' => 'edge-40',
'samsung' => 'galaxy-s-24',
'xiaomi' => 'xiaomi-13',
][ $brand ],
'pa_barva' => [
"bezova",
"cerna"
][1],
]);
// Save the product
$product->save();
// Fake a request to generate variations
$woo = new WC_REST_Product_Variations_Controller();
$fake_request_data = new WP_REST_Request('POST', '/wp-json/wc/v3/products/'.$product_id.'/variations/generate');
$fake_request_data->set_param('product_id', $product_id);
$fake_request_data->set_param('delete', 1);
$res = $woo->generate( $fake_request_data );
if ($res->status === 200) {
$log['generate_variations'] = $res->data;
} else {
$log['generate_variations'] = $res->errors;
}
/** @var $variation_ids int[] */
$variation_ids = $product->get_children();
foreach ($variation_ids as $variation_id) {
$variation = wc_get_product_object( 'variation', $variation_id );
$variation->set_regular_price( SHOP_PRICE );
$variation->save();
// $variation = wc_get_product($variation_id);
// $variation->set_regular_price(SHOP_PRICE);
// $variation->set_price(SHOP_PRICE);
// $variation->set_stock_status('instock');
$log['variations'][$variation_id]['price'] = $variation->get_price();
}
$log = array_merge($log, [
'brand_attributes' => wc_get_product_terms( $product_id, 'pa_' . $brand, ['fields' => 'slugs'] ),
'color_attributes' => wc_get_product_terms( $product_id, 'pa_barva', ['fields' => 'slugs'] ),
'default_attributes' => wc_get_product($product_id)->get_default_attributes(),
]);
return rest_ensure_response($log);
},
]);
});Response representation
Response representation:
{
"ok": true,
"generate_variations": {
"count": 0,
"deleted_count": 0
},
"variations": {
"17677": {
"price": "599"
},
"17678": {
"price": "599"
},
"17679": {
"price": "599"
},
"17680": {
"price": "599"
}
},
"brand_attributes": [
"edge-20",
"edge-40"
],
"color_attributes": [
"bezova",
"cerna"
],
"default_attributes": {
"pa_motorola": "edge-40",
"pa_barva": "cerna"
}
}Programmatically triggering the update for products one by one
Now, when we have the variations generated, we need to update the product variations attributes programmatically
local API_BASE='https://www.doffcases.test/wp-json/devices'
# Get the product IDs
# - all: '[.product_ids[]]'
# - or specific: '[.iphone_product_ids[], .motorola_product_ids[], .samsung__product_ids[], xiaomi_product_ids[]]'
PRODUCTS_IDS=$(curl -s -X GET "${API_BASE}/debug" | jq -r '[.motorola_product_ids[]] | @sh')
# Convert JSON array into a bash array
eval "PRODUCTS_IDS=($PRODUCTS_IDS)"
# Log all product IDs
echo "Processing ${#PRODUCTS_IDS[@]} products one by one, this can take a while..." && echo "Follow me to support:" && echo "--------------------------------" && echo "🔗 https://linkedin.com/in/andyivashchuk" && echo "💬 https://t.me/digitalandyeu" && echo "💻 https://github.com/andriilive"
echo "--------------------------------"
# Loop through the product IDs
for PRODUCT_ID in "${PRODUCTS_IDS[@]}"; do
echo "Processing ID: ${PRODUCT_ID}..."
# Send API request and capture response
resp=$(curl -s -X GET "${API_BASE}/${PRODUCT_ID}/generate")
# Log the response
echo "Generated Variations for ID ${PRODUCT_ID}: ${resp}"
echo "--------------------------------"
doneScript evaluation example:
- Reduced the numbers of routines
- Provided WooCommerce attribute sync functionality
- Scalable API-driven automation









