WooCommerce variable products are products that have multiple purchasable variants — each combination of attribute values (e.g., Size: Large + Color: Red) corresponds to a product variation, which is a child post of type product_variation with its own SKU, price, stock level, and image. Creating a variable product programmatically requires four steps: create the parent product as a WC_Product_Variable object, register the attributes on the product using $product->set_attributes() with WC_Product_Attribute objects, save the product, and then create each variation as a WC_Product_Variation object with the parent ID, the attribute combination, and the pricing data. Product attributes can be global (registered as pa_size taxonomy terms) or local (stored in the product meta as a custom attribute array) — global attributes allow filtering in the shop and share terms across products, while local attributes are simpler for one-off configurations. The WC_Product_Attribute object requires set_id() (taxonomy ID for global, 0 for local), set_name(), set_options() (array of term slugs or option strings), set_position(), set_visible(), and set_variation(true) to mark it as used for variations. Each variation is created by instantiating WC_Product_Variation, setting the parent ID, calling set_attributes() with the specific value combination, setting price, stock, and SKU, then saving. After creating all variations, WC_Product_Variable::sync() must be called with the parent product ID to recalculate the parent’s price range displayed in the shop. Bulk-importing variable products from a spreadsheet or ERP system uses this same programmatic flow in a WP-CLI command or a background task running via Action Scheduler. The custom product fields post shows how to extend this pattern with additional meta fields on both the parent product and individual variations.
Problem: A store migrating from a legacy platform needs to import 500 clothing products, each with up to 12 size/color variation combinations, into WooCommerce — doing it manually through the admin UI would take days and is error-prone.
Solution: Write a function that creates a WC_Product_Variable parent with global attribute terms, then loops over a variation matrix to create each WC_Product_Variation with its specific attribute combination, price, SKU, and stock, and calls WC_Product_Variable::sync() to update the parent price range.
/**
* Create a variable product with size and color variations.
*
* @param array $data {
* string $name, string $description,
* array $variations [ ['size'=>'M','color'=>'Red','price'=>29.99,'sku'=>'SHIRT-M-RED','stock'=>10], ... ]
* }
* @return int Parent product ID
*/
function myplugin_create_variable_product(array $data): int {
// 1. Create parent variable product
$product = new WC_Product_Variable();
$product->set_name(sanitize_text_field($data['name']));
$product->set_description(wp_kses_post($data['description'] ?? ''));
$product->set_status('publish');
$product->set_catalog_visibility('visible');
// 2. Register global attributes (pa_size and pa_color must exist as taxonomies)
$size_terms = array_unique(array_column($data['variations'], 'size'));
$color_terms = array_unique(array_column($data['variations'], 'color'));
$size_attr = new WC_Product_Attribute();
$size_attr->set_id(wc_attribute_taxonomy_id_by_name('pa_size'));
$size_attr->set_name('pa_size');
$size_attr->set_options(array_map('sanitize_title', $size_terms));
$size_attr->set_position(0);
$size_attr->set_visible(true);
$size_attr->set_variation(true);
$color_attr = new WC_Product_Attribute();
$color_attr->set_id(wc_attribute_taxonomy_id_by_name('pa_color'));
$color_attr->set_name('pa_color');
$color_attr->set_options(array_map('sanitize_title', $color_terms));
$color_attr->set_position(1);
$color_attr->set_visible(true);
$color_attr->set_variation(true);
$product->set_attributes([$size_attr, $color_attr]);
$parent_id = $product->save();
// 3. Create each variation
foreach ($data['variations'] as $v) {
$variation = new WC_Product_Variation();
$variation->set_parent_id($parent_id);
$variation->set_attributes([
'pa_size' => sanitize_title($v['size']),
'pa_color' => sanitize_title($v['color']),
]);
$variation->set_regular_price((string)(float)$v['price']);
$variation->set_sku(sanitize_text_field($v['sku']));
$variation->set_stock_quantity(absint($v['stock']));
$variation->set_manage_stock(true);
$variation->set_status('publish');
$variation->save();
}
// 4. Sync parent price range and stock status
WC_Product_Variable::sync($parent_id);
return $parent_id;
}
// Usage example
$product_id = myplugin_create_variable_product([
'name' => 'Classic T-Shirt',
'description'=> '100% cotton unisex tee.',
'variations' => [
['size' => 'S', 'color' => 'White', 'price' => 24.99, 'sku' => 'TEE-S-WHT', 'stock' => 20],
['size' => 'M', 'color' => 'White', 'price' => 24.99, 'sku' => 'TEE-M-WHT', 'stock' => 35],
['size' => 'M', 'color' => 'Black', 'price' => 24.99, 'sku' => 'TEE-M-BLK', 'stock' => 28],
['size' => 'L', 'color' => 'Black', 'price' => 26.99, 'sku' => 'TEE-L-BLK', 'stock' => 15],
],
]);
echo "Created product ID: $product_id";
NOTE: Global attribute terms (pa_size, pa_color) must exist in the database before setting them on a product — use wp_insert_term('Large', 'pa_size') to create missing terms, or use wc_create_attribute() to register the attribute taxonomy itself if it does not exist. Running WC_Product_Variable::sync() after bulk creation is important — skipping it leaves the parent showing an incorrect or empty price range in the shop catalog.