Skip to content

Instantly share code, notes, and snippets.

@andriilive
Last active March 14, 2025 14:58
Show Gist options
  • Select an option

  • Save andriilive/f5a6b9347376b5bb80906d8ff1d697f5 to your computer and use it in GitHub Desktop.

Select an option

Save andriilive/f5a6b9347376b5bb80906d8ff1d697f5 to your computer and use it in GitHub Desktop.
WooCommerce: setting product attributes and generating variations programmaticaly

WooCommerce: programaticly setting product attributes and generating variations (PHP / Bash)

StandWithUkraine  Github Badge  Github Badge

Managing variable products in WooCommerce—especially with multiple attributes—is often a manual nightmare for eCommerce store owners, you'll quickly find yourself struggling.

Woocommerce multiple variable products administration is a hell (case study)

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

Increase the product variation generation limit

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 # restore

I do recomend making manipulations in local enviorment, then exporting the changed db for production

⚠️ Localhost only the routes should be protected for production enviorment

Query the data and debug

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
}

Automating the variable product routines

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"
  }
}

Updating products one by one (Bash)

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 "--------------------------------"
done

Script evaluation example:

CleanShot 2025-02-24 at 06 23 32@2x


Variable products routine case study

  • Reduced the numbers of routines
  • Provided WooCommerce attribute sync functionality
  • Scalable API-driven automation

Follow me on: LinkedIn | 💬 Telegram

List of sources:

Source: https://rudrastyh.com/woocommerce/create-product-programmatically.html
Author: Misha Rudrastyh

Create Products Programmatically

Updated on February 10, 2025

In this tutorial, I am going to guide you through the process of creating products in WooCommerce in code. The same way of creating products I use when crossposting products between stores in a multisite network, so, in case you need more examples, you can check out this article or my crossposting plugin.

It is the most correct way of creating products in the latest WooCommerce versions. So, please forget about wp_insert_post() and update_post_meta() functions. Yes, I perfectly remember that products in WooCommerce are (still) WordPress custom post types and product prices are post meta but it doesn’t allow us to use those functions anyway, because there is a little more than that and it could cause bugs.

In this tutorial, we are going to use CRUD objects that were introduced in WooCommerce 3.0 (CRUD is an abbreviation of Create, Read, Update, Delete). The CRUD objects we are interested in right now are:

  • WC_Product_Simple – for simple products,
  • WC_Product_External – for external products,
  • WC_Product_Grouped – for grouped products,
  • WC_Product_Variable – variable ones.

We are also going to use plenty of methods of these objects, that allow us to configure our product data.

Simple Products 

Let’s start with creating a simple product. I am suggesting you imagine, what an example simple product should have. Well, it starts with the product name, slug (for URL), price, and… image, right? Okay, we may have a product description with some nice sale lines as well!

// that's a CRUD object $product = new WC_Product_Simple(); $product->set_name( 'Wizard Hat' ); // product title $product->set_slug( 'medium-size-wizard-hat-in-new-york' ); $product->set_regular_price( 500.00 ); // in current shop currency $product->set_short_description( '<p>Here it is... A WIZARD HAT!</p><p>Only here and now.</p>' ); // you can also add a full product description // $product->set_description( 'long description here...' ); $product->set_image_id( 90 ); // let's suppose that our 'Accessories' category has ID = 19 $product->set_category_ids( array( 19 ) ); // you can also use $product->set_tag_ids() for tags, brands etc $product->save();

The code above is enough to create a product like this.

Create a Simple WooCommerce product programmatically

Newly created product, Storefront theme.

There are also methods that you could find useful:

  • set_featured() – pass true if you want this product to be marked as featured.
  • set_gallery_image_ids() – multiple image IDs can be passed as an array here.
  • set_menu_order() – manual product order as an integer.
  • set_status() – you can set a post status using this method. Don’t want the product to be published? Set draft here.
  • set_total_sales() – product total sales can be passed here as an integer value.
  • set_catalog_visibility() – visibility in the catalog can be configured with this method, hidden, visible, search and catalog values are accepted.
  • update_meta_data() – any product meta data, pass a pair of meta key and meta value as first and second parameters appropriately.

Products on Sale 

You can use it in combination with the snippet above, but please don’t forget to add these methods before $product->save(); line.

$product->set_regular_price( 500.00 ); $product->set_sale_price( 250.00 ); // sale schedule $product->set_date_on_sale_from( '2022-05-01' ); $product->set_date_on_sale_to( '2022-05-31' );

Creating a product on sale in WooCommerce programmatically

The product price has been changed and depending on a theme you’re using Sale! could appear, by the way there is a different tutorial on how to remove or modify it.

Inventory Settings 

$product->set_sku( 'wzrd-hat' ); // Should be unique // You do not need it if you manage stock at product level (below) $product->set_stock_status( 'instock' ); // 'instock', 'outofstock' or 'onbackorder' // Stock management at product level $product->set_manage_stock( true ); $product->set_stock_quantity( 5 ); $product->set_backorders( 'no' ); // 'yes', 'no' or 'notify' $prodict->set_low_stock_amount( 2 ); $product->set_sold_individually( true );

The parameters above lead to this Inventory configuration.

product inventory settings while creating a product in code

Dimensions and Shipping 

$product->set_weight( 0.5 ); $product->set_length( 50 ); $product->set_width( 50 ); $product->set_height( 30 ); $product->set_shipping_class_id( 'hats-shipping' );

In case you’re wondering what is a shipping class ID and where you can get it, I recommend you check this tutorial.

Linked Products 

There are two methods – set_upsell_ids() and set_cross_sell_ids(), both of them accept an array of product IDs.

$product->set_upsell_ids( array( 15, 17 ) ); // we can do the same for cross-sells // $product->set_cross_sell_ids( array( 15, 17, 19, 210 ) );

WooCommerce product upsells

Attributes 

This is going to be interesting.

First of all, I want to remind you that there are two types of product attributes in WooCommerce – predefined taxonomy-based attributes (which can be created in Products > Attributes) and individual product attributes.

Second, forget about wp_set_object_terms() here.

No matter what type of attribute you are going to add to a product, you have to use the single method which is set_attributes(). But what should we pass into it? Below is an example of how to use WC_Product_Attribute class in order to add a custom or taxonomy-based attribute to a product programmatically.

// that's going to be an array of attributes we add to a product programmatically $attributes = array(); // add the first attribute $attribute = new WC_Product_Attribute(); $attribute->set_name( 'Magical' ); $attribute->set_options( array( 'Yes', 'No' ) ); $attribute->set_position( 0 ); $attribute->set_visible( true ); $attribute->set_variation( true ); $attributes[] = $attribute; // add the second attribute, it is predefined taxonomy-based attribute $attribute = new WC_Product_Attribute(); $attribute->set_id( wc_attribute_taxonomy_id_by_name( 'pa_color' ) ); $attribute->set_name( 'pa_color' ); $attribute->set_options( array( 29, 31 ) ); $attribute->set_position( 1 ); $attribute->set_visible( true ); $attribute->set_variation( false ); $attributes[] = $attribute; $product->set_attributes( $attributes );

When creating attributes you have to remember two things:

  • The difference between creating custom or taxonomy-based attributes is in what you pass into set_id() and set_name() methods. It should be an attribute taxonomy name inside the wc_attribute_taxonomy_id_by_name() function and just a taxonomy name appropriately.
  • When you pass term IDs in set_option() function, you shouldn’t do it too early in the code when taxonomy is not even registered. It also applies to product categories and tags.

Create product attributes WC_Product_Attribute in code

Virtual and Downloadable products 

Let’s assume that our Wizard Hat is just an illustration that we are going to purchase or an accessory in a game like Lineage II. How to make it virtual and downloadable as well?

It is kind of similar to how we made it for product attributes.

// Virtual product : YES $product->set_virtual( true ); // Downloadable product : YES $product->set_downloadable( true ); $downloads = array(); // Creating a download with... yes, WC_Product_Download class $download = new WC_Product_Download(); $file_url = wp_get_attachment_url( $attachment_id ); // attachmend ID should be here $download->set_name( 'wizard-hat-illustration' ); $download->set_id( md5( $file_url ) ); $download->set_file( $file_url ); $downloads[] = $download; $product->set_downloads( $downloads ); $product->set_download_limit( 1 ); // can be downloaded only once $product->set_download_expiry( 7 ); // expires in a week

how to create virtual and downloadable products

External and Grouped products 

Creating both external and grouped products programmatically is quite simple. If you read the previous chapter carefully, you won’t have any problems with it at all.

First of all, you have to choose an appropriate product PHP class, which is WC_Product_External for external products and WC_Product_Grouped for grouped ones.

Then you have to use set_button_text() and set_product_url() methods to add a link to a partner website where this external product is listed. For grouped products, you have to use the set_children() method only.

Variable Products 

Well, it is going to be interesting again. But as always we will make it as simple as possible.

// Creating a variable product $product = new WC_Product_Variable(); // Name and image would be enough $product->set_name( 'Wizard Hat' ); $product->set_image_id( 90 ); // one available for variation attribute $attribute = new WC_Product_Attribute(); $attribute->set_name( 'Magical' ); $attribute->set_options( array( 'Yes', 'No' ) ); $attribute->set_position( 0 ); $attribute->set_visible( true ); $attribute->set_variation( true ); // here it is $product->set_attributes( array( $attribute ) ); // save the changes and go on $product->save(); // now we need two variations for Magical and Non-magical Wizard hat $variation = new WC_Product_Variation(); $variation->set_parent_id( $product->get_id() ); $variation->set_attributes( array( 'magical' => 'Yes' ) ); $variation->set_regular_price( 1000000 ); // yep, magic hat is quite expensive $variation->save(); $variation = new WC_Product_Variation(); $variation->set_parent_id( $product->get_id() ); $variation->set_attributes( array( 'magical' => 'No' ) ); $variation->set_regular_price( 500 ); $variation->save();

Of course, the WC_Product_Variation class has many more methods, almost like the WC_Product_Simple all of them you could find in official WooCommerce documentation.

But for now – we have this simple variable product in our shop.

create a variable product programmatically

Almost forgot, you can set a default variation with the set_default_attributes() method.

$product->set_default_attributes( array( 'magical' => 'No' ) );

One more thing, worth noting – what to do if you want to assign global, taxonomy-based attributes to a variation? Here is what:

$variation->set_attributes( array( 'attribute_pa_magical' => 'yes', 'attibute_pa_color' => 'gray', ) );

Basically, you just need to pass an associative array to the set_attributes() method, using attribute_{$taxonomy_name} as keys and attribute term slugs as values.


Notes

Earlier experiments

Earlier experiments

1. Querying the Data

First of all, let's create a simple API endpoint to retrieve testing data and some utility functions to simplify the products query

<?php
const SHOP_BRANDS = ['motorola', 'iphone', 'samsung', 'xiaomi'];

// 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 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|array $category = 'kolekce'
 *
 * @returns int[] array of phone product IDs
 *
 * @see WC_Product_Query
 * */
function 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,
    ]));
}

/**
 * Get all term options ids by brand name
 * @param string $brand = 'motorola' | 'iphone' | 'samsung' | 'xiaomi'
 * @returns WP_Term[] array of term objects
 * @see WP_Term_Query
 * */
function get_attribute_options_ids_by_name(
    string $brand = 'motorola' | 'iphone' | 'samsung' | 'xiaomi'
): array
{
    // Get all term options
    return get_terms([
        'taxonomy' => 'pa_' . $brand,
        'hide_empty' => false,
    ]);
}

/**
* Enables REST API endpoint for querying all required data for testing purposes
* @var $products int[]
* @return WP_REST_Response|WP_Error
* */
function query_cases_route_callback() : WP_REST_Response|WP_Error
{
    global $woocommerce;
    $results['pa_barva'] = ['id' => 5, 'options' => [106, 105]]; // $results['pa_barva'] = get_terms(['taxonomy' => 'pa_barva','hide_empty' => false,]);
    $results = [];

    // Get the ID's for each term
    foreach (SHOP_BRANDS as $brand) {

        /** @var $terms WP_Term[] */
        $terms = shop_get_attribute_options_ids_by_name($brand);

        // Prepare the results array
        $temp_results = [
            'pa_' . $brand . '_count' => count($terms),
        ];

        // Loop through the terms
        foreach ($terms as $term) {
            $temp_results['pa_' . $brand . '_ids'][] = $term->term_id;
            $temp_results['pa_' . $brand . '_index'][] = [
                'id' => $term->term_id,
                'name' => $term->name,
                'count' => $term->count,
            ];
        }

        $products_ids = shop_query_cases_by_brand($brand);

        $results['products_' . $brand . '_ids'] = $products_ids;
        $results['products_' . $brand . '_count'] = count($products_ids);
        $results = array_merge($results, $temp_results);
    }
    return rest_ensure_response($results);
}

/** @see https://www.doffcases.test/wp-json/devices/all */
add_action( 'rest_api_init', function () {
    register_rest_route( 'devices', '/all', [
        'methods'  => WP_REST_Server::READABLE,
        'callback' => 'query_cases_route_callback',
    ]);
});

Now we can visit the route https://doffcases.test/wp-json/devices/all to check that we retrieve all the data correctly.

See the expected response representation:

{
  "products_motorola_ids": [
    15173,
    14958
  ],
  "products_motorola_count": 2,
  "pa_motorola_count": 2,
  "pa_motorola_ids": [
    231,
    232
  ],
  "pa_motorola_index": [
    {
      "id": 231,
      "name": "Edge 20",
      "count": 2
    },
    {
      "id": 232,
      "name": "Edge 40",
      "count": 2
    }
  ],
  // ...
}

1. Programmatically Setting Product Attributes

Now let's define some utility functions and endpoint to programmatically set product attributes

/**
 * Set attribute helper
 * @param string $pa_taxonomy_name
 * @param array $pa_taxonomy_options
 * @return array
 */
function set_attribute_helper(
    string $pa_taxonomy_name,
    array $pa_taxonomy_options = []
) : array
{
    $attribute_object = new WC_Product_Attribute();
    $attribute_object->set_name( $pa_taxonomy_name );

    // Set options to provided values or get all term ids
    $attribute_object->set_options( empty($pa_taxonomy_options) ? array_map(fn (WP_Term $term) => $term->term_id, get_terms([
        'taxonomy' => $pa_taxonomy_name,
        'hide_empty' => false,
    ])) : $pa_taxonomy_options );

    $attribute_object->set_visible( true );
    $attribute_object->set_variation( true );

    $taxonomy_id = wc_attribute_taxonomy_id_by_name( $pa_taxonomy_name );

    $attribute_object->set_id( $taxonomy_id );
    $attributes[$pa_taxonomy_name] = $attribute_object;

    return $attributes;
}

/**
 * Add variations to all products
 * @return array log
 * */
function set_attributes() : array
{
    global $woocommerce;
    $log = [];

    foreach (SHOP_BRANDS as $brand) {
        foreach (shop_query_cases_by_brand($brand) as $product_id) {

            /** @var $product WC_Product */
            $product = wc_get_product($product_id);

            /** @var $attributes WC_Product_Attribute[] */
            $attributes = $product->get_attributes();

            // reset the attributes
            $attributes = [];

            // Brand attribute
            $brand_attribute_name = 'pa_' . $brand; // 'pa_motorola' | 'pa_iphone' | 'pa_samsung' | 'pa_xiaomi'
            $attributes[$brand_attribute_name] = shop_set_attribute_helper($brand_attribute_name);

            // Color attribute
            $color_attribute_name = 'pa_barva';
            // TODO: clean up colors to include programmatically
            $attributes[$color_attribute_name] = shop_set_attribute_helper($color_attribute_name, [106, 105]);

            $log[] = [
                'product_id' => $product_id,
                'brand_attributes' => wc_get_product_terms( $product_id, $brand_attribute_name, ['fields' => 'all'] ),
                'color_attributes' => wc_get_product_terms( $product_id, $color_attribute_name, ['fields' => 'all'] ),
                'default_attributes' => wc_get_product($product_id)->get_default_attributes(),
            ];
        }
    }
    return $log;
}

/**
 * TODO: protect this endpoint
 * @see https://www.doffcases.test/wp-json/devices/set_attributes
 * */
add_action( 'rest_api_init', function () {
    register_rest_route( 'devices', '/set_attributes', [
        'methods'  => WP_REST_Server::READABLE,
        'callback' => fn()=> rest_ensure_response(shop_set_attributes()),
    ]);
});

Visit the route https://doffcases.test/wp-json/devices/set_attributes to set the attributes for all products.

⚠️ Warning! This endpoint is not protected, so be careful with the production environment

Response example json representation:

[
    {
      "product_id": 15173,
      "brand_attributes": [
        {
          "term_id": 231,
          "name": "Edge 20",
          "slug": "edge-20",
          "term_group": 0,
          "term_taxonomy_id": 231,
          "taxonomy": "pa_motorola",
          "description": "",
          "parent": 0,
          "count": 2,
          "filter": "raw"
        },
        {
          "term_id": 232,
          "name": "Edge 40",
          "slug": "edge-40",
          "term_group": 0,
          "term_taxonomy_id": 232,
          "taxonomy": "pa_motorola",
          "description": "",
          "parent": 0,
          "count": 2,
          "filter": "raw"
        }
      ],
      "color_attributes": [
        {
          "term_id": 106,
          "name": "Béžová",
          "slug": "bezova",
          "term_group": 0,
          "term_taxonomy_id": 106,
          "taxonomy": "pa_barva",
          "description": "",
          "parent": 0,
          "count": 315,
          "filter": "raw"
        },
        {
          "term_id": 105,
          "name": "Černá",
          "slug": "cerna",
          "term_group": 0,
          "term_taxonomy_id": 105,
          "taxonomy": "pa_barva",
          "description": "",
          "parent": 0,
          "count": 315,
          "filter": "raw"
        }
      ],
      "default_attributes": []
    },
    // ...
]

3. Programmatically Generating Product Variations

Finally, let's define the utility functions and endpoint to programmatically generate product variations.

The variations are generated by the REST API POST call, sent to the WooCommerce API.

Prequirements:

  • Ensure the WooCommerce API is enabled
  • Get the consumer_key and consumer_secret for the WooCommerce API
  • ⚠️ Requires the jq tool to parse the JSON response

3.0 WooCommerce API Setup

Ensure the WooCommerce API is enabled. Get the consumer_key and consumer_secret for the WooCommerce API.

⚠️ Warning! Make sure you're requesting the API with the https protocol

I use laravel valet as a local development environment, so I can fake the https protocol with the valet secure command.

Read the official documentation on how to set up the WooCommerce API: https://woocommerce.com/document/woocommerce-rest-api

Send a bunch of test requests to the API with the Postman or similar tool to ensure the API is working correctly.

The API endpoint should look like this: https://doffcases.test/wp-json/wc/v3/products?consumer_key=ck_aaa&consumer_secret=cs_aaa

I'd recommend using the Postman tool to test the API requests or some other REST API client.

Test the API call can look like this:

img_1.png

GET request representation with the curl command:

local API_BASE='https://www.doffcases.test' DEV_CS='cs_...' DEV_CK='ck_...'
local PRODUCT_ID=15173
curl -X GET "${API_BASE}/wp-json/wc/v3/products/${PRODUCT_ID}" --user "${DEV_CK}:${DEV_CS}"

Preserve the environment variables:

Preserve the environment variables for the API base URL, consumer key, and consumer secret for the current shell session

export API_BASE='https://www.doffcases.test' 
export DEV_CS='cs_...'
export DEV_CK='ck_...'

3.1 Retrieving the Product IDs

Let's retrieve the product IDs and generate the variations for each product

local PRODUCTS_IDS=$(curl -s -X GET "${API_BASE}/wp-json/devices/all" | jq '[.products_motorola_ids[], .products_iphone_ids[], .products_samsung_ids[], .products_xiaomi_ids[]]')
echo $PRODUCTS_IDS

Response representation:

[
  15173,
  14958,
  // ...
]

3.1 Increasing the Variations generation Limit (optional)

If the variations count exceeds the default limit of 50, we need to increase the limit:

// wp-config.php
define('WC_MAX_LINKED_VARIATIONS', 100);

3.2 Sending the test request to the API

To generate the variations, we need to send a POST request to the WooCommerce API, here is a template:

{{DEV_WC_V3}}/products/{{PRODUCT_ID}}/variations/generate?consumer_key={{DEV_CK}}&consumer_secret={{DEV_CS}}

Send single test request to the API with the Postman or similar tool to ensure the API is working correctly.

https://doffcases.test/wp-json/wc/v3/products/15173/variations/generate?consumer_key=ck_aaa&consumer_secret=cs_aaa

POST request representation with the curl command:

curl -X POST "${API_BASE}/wp-json/wc/v3/products/${PRODUCT_ID}/variations/generate" --user "${DEV_CK}:${DEV_CS}"

the request should return the generated variations count {"count":62}

3.3 Wrapping the script up

Finally, let's wrap the script up to loop through the products and generate the variations for each product

#!/bin/bash

# Set the environment variables
local API_BASE='https://www.doffcases.test' 
local DEV_CS='cs_...'
local DEV_CK='ck_...'

# Get the product IDs correctly as an array
PRODUCTS_IDS=$(curl -s -X GET "${API_BASE}/wp-json/devices/all" | jq -r '[.products_motorola_ids[], .products_iphone_ids[], .products_samsung_ids[], .products_xiaomi_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 "🔗 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 POST "${API_BASE}/wp-json/wc/v3/products/${PRODUCT_ID}/variations/generate" \
        --user "${DEV_CK}:${DEV_CS}" \
        -H "Content-Type: application/json")
    
    # Log the response
    echo "Generated Variations for ID ${PRODUCT_ID}: ${resp}"
done

Screenshot of the script execution:

img_4.png

4. Programmatically Updating the Product Variations

Now, when we have the variations generated, we need to update the product variations attributes programmatically.

See the woo API documentation on how to update the product variations: https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#list-all-product-variations

#!/bin/bash

# Set the environment variables
local API_BASE='https://www.doffcases.test' 
local DEV_CS='cs_...'
local DEV_CK='ck_...'

# Get the product IDs correctly as an array
PRODUCTS_IDS=$(curl -s -X GET "${API_BASE}/wp-json/devices/all" | jq -r '[.products_motorola_ids[], .products_iphone_ids[], .products_samsung_ids[], .products_xiaomi_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 "🔗 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 POST "${API_BASE}/wp-json/wc/v3/products/${PRODUCT_ID}/variations" \
        --user "${DEV_CK}:${DEV_CS}" \
        -H "Content-Type: application/json")
    
    # Log the response
    echo "Generated Variations for ID ${PRODUCT_ID}: ${resp}"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment