{"slug":"wc-variations-data","title":"wc-variations-data","summary":"Read, query, and write WooCommerce product variations through `WC_Product_Variation` and `WC_Product_Variable`. Covers parent/child storage, cached variation prices, deferred parent synchronization, WooCommerce 11.0 product instance caching, stock inheritance, frontend variation ","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:20.10447Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: wc-variations-data\ndescription: Read, query, and write WooCommerce product variations through <code>WC_Product_Variation</code> and <code>WC_Product_Variable</code>. Covers parent/child storage, cached variation prices, deferred parent synchronization, WooCommerce 11.0 product instance caching, stock inheritance, frontend variation data, and raw versus display-tax price reads. Use when creating, importing, querying, or debugging stale variation data.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"woocommerce\"\nwp-skills-plugin-version-tested: \"11.0.0\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-05\"</h2>\n<h1>WooCommerce: variations data layer (CRUD + cache)</h1>\n<p>For plugin code that <strong>reads, queries, or programmatically writes</strong> product variations. The pricing/display side (filter chain, sale-price overrides, frontend \"Variation X is selected\" hooks) is sibling skill <code>wc-variations-pricing-filters</code> — this one stops at the data layer.</p>\n<h2>Misconception this skill corrects</h2>\n<blockquote>\n<p>\"A variation is just a product. I'll <code>wp_insert_post( 'product_variation', ... )</code> and <code>update_post_meta</code> for price.\"</p>\n</blockquote>\n<p>Variations work that way at the database level, but the data layer caches aggressively at the parent level. A raw post-insert leaves the parent's stored <code>_price</code> values, lookup table row, child-list transient, and <code>wc_var_prices_&lt;id&gt;</code> transient stale — your variation exists but the catalog shows the OLD price range, \"Out of stock\" stays on a re-stocked variable product, and frontend variation data may not see the new child until caches are rebuilt.</p>\n<h2>When to use this skill</h2>\n<p>Trigger when ANY of the following is true:</p>\n<ul>\n<li>Programmatically creating, updating, or deleting variations (import scripts, sync from external system, CLI tools).</li>\n<li>Reading variation prices for catalog display, custom listings, or reports.</li>\n<li>Debugging \"I added a variation programmatically and the parent's price range / stock didn't update\".</li>\n<li>Reviewing code where variations are touched via <code>wp_insert_post</code>, <code>wp_update_post</code>, <code>update_post_meta</code>, or raw <code>$wpdb</code> writes against <code>product_variation</code> post type.</li>\n<li>The diff or file contains: <code>WC_Product_Variation</code>, <code>WC_Product_Variable</code>, <code>get_variation_prices</code>, <code>get_available_variations</code>, <code>wc_var_prices_</code>, <code>sync_variation_names</code>, or <code>'product_variation'</code> post type references.</li>\n</ul>\n<h2>Mental model — two classes, one tree</h2>\n<pre><code>Variable product (parent)\n  post_type     = 'product'\n  class         = WC_Product_Variable\n  WP post id    = 100\n  Holds:        product attributes (Color: Red, Blue; Size: S, M, L)\n                cached min/max prices, derived stock status\n\n  └── Variation (child)\n      post_type   = 'product_variation'\n      class       = WC_Product_Variation\n      post_parent = 100\n      WP post id  = 101, 102, 103, ...\n      Holds:      attribute values (attribute_pa_color = red, attribute_pa_size = m)\n                  own price, own stock, own SKU\n</code></pre>\n<p>The parent stores aggregated / derived data; each child variation stores its own concrete values. Almost every \"stale data\" bug comes from writing to a child without telling the parent to re-aggregate.</p>\n<h2>The price aggregation cache — <code>wc_var_prices_&lt;id&gt;</code></h2>\n<p><code>WC_Product_Variable::get_variation_prices( $for_display )</code> (<a href=\"class-wc-product-variable.php\">includes/class-wc-product-variable.php:99</a>) returns an array shaped:</p>\n<pre><code>array(\n    'price'         =&gt; array( 101 =&gt; '15.00', 102 =&gt; '18.00', 103 =&gt; '20.00' ), // sorted\n    'regular_price' =&gt; array( ... ),\n    'sale_price'    =&gt; array( ... ),\n)\n</code></pre>\n<p>The aggregation is backed by a transient named <code>wc_var_prices_&lt;parent_id&gt;</code> (<a href=\"class-wc-product-variable-data-store-cpt.php\">includes/data-stores/class-wc-product-variable-data-store-cpt.php</a>). The transient stores entries keyed by:</p>\n<ul>\n<li>A <strong>transient version</strong> from <code>WC_Cache_Helper::get_transient_version( 'product' )</code> (busted by <code>wc_delete_product_transients()</code>)</li>\n<li>A <strong>price hash</strong> built from current tax display settings, customer VAT-exempt status, rate table, and the active <code>woocommerce_variation_prices_*</code> callbacks — different hashes for \"include tax\" vs \"exclude tax\", different for VAT-exempt vs not, etc.</li>\n</ul>\n<p>The <code>$for_display</code> parameter matters:</p>\n<ul>\n<li><code>false</code> (default): RAW prices, before any tax adjustment. Use for storage / comparison / programmatic logic.</li>\n<li><code>true</code>: prices adapted for the <code>woocommerce_tax_display_shop</code> setting (include / exclude tax). Use ONLY when rendering UI.</li>\n</ul>\n<p>A common bug: read with <code>$for_display = true</code> then do business logic on the result — the value drifts depending on tax settings. Always read raw for logic, display-mode only at the render edge.</p>\n<h2>Programmatic variation creation — the right sequence</h2>\n<pre><code>$parent_id = 100; // an existing WC_Product_Variable\n\n$variation = new WC_Product_Variation();\n$variation-&gt;set_parent_id( $parent_id );\n\n// Attributes — keys are attribute slugs (taxonomy or custom), values are\n// the chosen term slug. Both must already exist on the parent.\n$variation-&gt;set_attributes( array(\n    'pa_color' =&gt; 'red',\n    'pa_size'  =&gt; 'm',\n) );\n\n$variation-&gt;set_regular_price( '20.00' );\n$variation-&gt;set_price( '20.00' );           // current price (= sale or regular)\n$variation-&gt;set_manage_stock( true );\n$variation-&gt;set_stock_quantity( 50 );\n$variation-&gt;set_stock_status( 'instock' );\n$variation-&gt;set_sku( 'TSHIRT-RED-M' );\n\n$variation_id = $variation-&gt;save();\n</code></pre>\n<p>In WooCommerce 11.0, variation CRUD clears variation and parent transients and <code>WC_Product::save()</code> queues the parent ID through <code>wc_deferred_product_sync()</code>. <code>WC_Post_Data::do_deferred_product_sync()</code> deduplicates and synchronizes parents at shutdown. Normal CRUD therefore needs no manual cache delete or parent sync.</p>\n<p>WooCommerce 11.0 enables <code>product_instance_caching</code> for newly installed stores while upgraded stores retain their previous state. Variation and parent CRUD invalidates that request cache; WordPress post/meta API writes also trigger its invalidation hooks, but raw SQL does not. Test both feature states and never depend on repeated <code>wc_get_product()</code> calls returning the identical PHP object.</p>\n<p>Direct post/meta/SQL writes bypass that contract and can leave the catalog stale. Avoid them. If the same request must read rebuilt parent aggregates before shutdown, call <code>WC_Product_Variable::sync( $parent_id )</code> explicitly after the final child write.</p>\n<h2>Querying variations</h2>\n<pre><code>// CHILDREN OF A PARENT — use $variable-&gt;get_children() (returns IDs)\n$variable = wc_get_product( $parent_id );\nif ( $variable instanceof WC_Product_Variable ) {\n    foreach ( $variable-&gt;get_children() as $variation_id ) {\n        $variation = wc_get_product( $variation_id );\n        // $variation is a WC_Product_Variation\n    }\n}\n\n// FRONTEND DISPLAY DATA — JSON-shaped for swatch / dropdown UIs\n$available = $variable-&gt;get_available_variations(); // array of arrays\n\n// Each entry has: variation_id, attributes, display_price, display_regular_price,\n// is_in_stock, max_qty, min_qty, image, sku, weight_html, dimensions_html, etc.\n</code></pre>\n<p><code>get_available_variations()</code> is what the classic variable-product template embeds inline when the variation count is below <code>woocommerce_ajax_variation_threshold</code> (default 30). Above that threshold, WC AJAX resolves one matching variation and calls <code>get_available_variation()</code> for that variation. The default array mode is heavy; if you only need objects, use <code>$variable-&gt;get_available_variations( 'objects' )</code>, or use <code>get_children()</code> and read only the properties you need.</p>\n<p>WooCommerce 11.0's matching-variation AJAX endpoint refuses to expose a non-published parent unless the current user can edit it. Custom variation lookup routes must preserve the same visibility boundary; never return unpublished/private variation data merely because the caller knows a parent ID and attributes.</p>\n<h3>REST v3 image size in WooCommerce 11.0</h3>\n<p>Authenticated <code>wc/v3/products</code> and <code>wc/v3/products/&lt;product_id&gt;/variations</code> collection requests now accept <code>image_size=&lt;registered-size&gt;</code>. It affects the generated image URL (and the product image <code>srcset</code>/<code>sizes</code>) but not stored attachment IDs. The default is <code>full</code>; an unregistered size falls back through WordPress image handling to the full image.</p>\n<p>Use a registered WordPress size such as <code>woocommerce_thumbnail</code> and keep clients tolerant of a full-size fallback. Do not persist an API presentation choice into product or variation metadata.</p>\n<h2>The variation stock inheritance model</h2>\n<table>\n<thead>\n<tr>\n<th>Setting on</th>\n<th>What it does</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Parent <code>manage_stock = 'yes'</code>, variation <code>manage_stock = 'no'</code></td>\n<td>Variation inherits the parent's stock quantity / backorders / stock status. Internally <code>WC_Product_Variation::get_manage_stock()</code> returns <code>'parent'</code>.</td>\n</tr>\n<tr>\n<td>Variation <code>manage_stock = 'yes'</code></td>\n<td>Per-variation stock. The variation has its own <code>stock_quantity</code> and is managed by its own ID even if the parent also manages stock. <strong>This is the common case for size/color inventory.</strong></td>\n</tr>\n<tr>\n<td>Parent <code>manage_stock = 'no'</code>, variation <code>manage_stock = 'no'</code></td>\n<td>Variation has only <code>stock_status</code> (<code>instock</code> / <code>outofstock</code> / <code>onbackorder</code>) without a quantity counter.</td>\n</tr>\n</tbody>\n</table>\n<p>Reading the effective stock status:</p>\n<pre><code>// Variation's own stock status (already accounts for managed-quantity rollover)\n$variation-&gt;get_stock_status(); // 'instock' / 'outofstock' / 'onbackorder'\n\n// Parent's display status — derived from children, set by sync()\n$variable-&gt;get_stock_status();\n</code></pre>\n<p>After CRUD stock changes, deferred parent sync recomputes the parent's display status at shutdown. Explicitly sync only when same-request code needs the updated parent immediately.</p>\n<h2>Changes that queue parent synchronization</h2>\n<p>CRUD writes affecting these values queue parent synchronization:</p>\n<ul>\n<li>Add a new variation</li>\n<li>Delete a variation</li>\n<li>Change a variation's regular_price / sale_price / price</li>\n<li>Change a variation's stock_quantity / stock_status / manage_stock</li>\n<li>Change a variation's enabled / disabled flag (post_status)</li>\n<li>Bulk update attributes that affect availability</li>\n</ul>\n<p>The shutdown queue deduplicates parent IDs, so CRUD-based imports do not need their own per-row sync. If code bypasses CRUD, it owns lookup-table updates, transient invalidation, and parent synchronization.</p>\n<h2>Hook checkpoints for variation data</h2>\n<p>Use these when a plugin needs to observe or extend variation data without taking over pricing filters:</p>\n<table>\n<thead>\n<tr>\n<th>Hook</th>\n<th>Use</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>woocommerce_new_product_variation</code></td>\n<td>A variation was created through CRUD. Args: variation ID, <code>WC_Product_Variation</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce_update_product_variation</code></td>\n<td>A variation was updated through CRUD. Good for external index refreshes.</td>\n</tr>\n<tr>\n<td><code>woocommerce_new_product_variation_data</code></td>\n<td>Last chance to alter the post array before <code>wp_insert_post()</code> creates a variation.</td>\n</tr>\n<tr>\n<td><code>woocommerce_available_variation</code></td>\n<td>Modify the frontend variation data array returned by <code>get_available_variation()</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce_hide_invisible_variations</code></td>\n<td>Decide whether disabled / empty-price variations are hidden from <code>get_available_variations()</code>.</td>\n</tr>\n<tr>\n<td><code>woocommerce_show_variation_price</code></td>\n<td>Decide whether selected variation price HTML is included in the frontend data array.</td>\n</tr>\n<tr>\n<td><code>woocommerce_variable_product_sync_data</code></td>\n<td>Parent variable product has just re-synced from children. Use for dependent caches.</td>\n</tr>\n</tbody>\n</table>\n<h2>Reading variation prices for display</h2>\n<pre><code>$variable = wc_get_product( $parent_id );\n\n// \"From €X to €Y\" range string — handles all the formatting + tax display\necho wp_kses_post( $variable-&gt;get_price_html() );\n\n// Manually if you need the raw values:\n$prices = $variable-&gt;get_variation_prices( true );  // for_display = true for UI\n$min    = current( $prices['price'] );\n$max    = end( $prices['price'] );\n</code></pre>\n<p>For non-display logic (filtering, sorting in custom listings), pass <code>$for_display = false</code> — raw prices, no tax adjustment.</p>\n<h2>Critical rules</h2>\n<ul>\n<li><strong><code>WC_Product_Variation</code> is the right class</strong> for programmatic create/update of a single variation. Don't <code>wp_insert_post( 'product_variation', ... )</code> directly.</li>\n<li><strong><code>set_parent_id</code> + <code>set_attributes</code> + <code>save()</code></strong> is the minimum sequence; attributes MUST match the parent's available attribute terms.</li>\n<li><strong>CRUD saves already invalidate and defer parent sync in WC 10.9.</strong> Do not add duplicate cache deletion/sync work after every row. Use explicit <code>WC_Product_Variable::sync()</code> only for immediate consistency or after unavoidable raw writes.</li>\n<li><strong><code>get_variation_prices( $for_display )</code> — always be explicit about <code>$for_display</code>.</strong> Default is <code>false</code> (raw); pass <code>true</code> only at the render edge.</li>\n<li><strong><code>get_children()</code> for cheap iteration</strong>, <code>get_available_variations()</code> for full UI-ready data — different costs.</li>\n<li><strong>Parent stock vs variation stock are independent flags.</strong> Most stores want variation-level (<code>manage_stock</code> on the variation, <code>manage_stock = no</code> on the parent).</li>\n<li><strong>Don't query variations via <code>WP_Query</code> / <code>get_posts</code> for hot-path code.</strong> Use <code>$variable-&gt;get_children()</code> and <code>wc_get_product()</code> per ID — the data store handles visible-child rules and cached child lists.</li>\n<li><strong>HPOS doesn't affect variations.</strong> Products and variations stay in <code>wp_posts</code> / <code>wp_postmeta</code> even with HPOS on (HPOS is order-table only). Variation reads / writes don't change between HPOS and legacy modes.</li>\n</ul>\n<h2>Common mistakes</h2>\n<pre><code>// WRONG — raw post insert leaves parent caches stale\n$variation_id = wp_insert_post( array(\n    'post_type'   =&gt; 'product_variation',\n    'post_parent' =&gt; $parent_id,\n    'post_status' =&gt; 'publish',\n) );\nupdate_post_meta( $variation_id, '_price', '20.00' );\n// Catalog shows old price range; frontend variation data may be stale.\n\n// RIGHT\n$variation = new WC_Product_Variation();\n$variation-&gt;set_parent_id( $parent_id );\n$variation-&gt;set_attributes( array( 'pa_color' =&gt; 'red' ) );\n$variation-&gt;set_regular_price( '20.00' );\n$variation-&gt;set_price( '20.00' );\n$variation-&gt;save();\n\n// WRONG — passing $for_display = true and then doing logic on the value\n$prices = $variable-&gt;get_variation_prices( true );\nif ( current( $prices['price'] ) &lt; 10 ) { /* ... */ }\n// On a tax-inclusive store, current('price') is post-tax — your &lt; 10 threshold is wrong.\n\n// RIGHT — read raw for logic, display-mode only at the render edge\n$prices = $variable-&gt;get_variation_prices( false );\nif ( (float) current( $prices['price'] ) &lt; 10 ) { /* ... */ }\n\n// RIGHT — CRUD queues and deduplicates parent sync at shutdown.\nforeach ( $rows as $row ) {\n    $v = new WC_Product_Variation();\n    $v-&gt;set_parent_id( $row['parent_id'] );\n    $v-&gt;set_regular_price( $row['price'] );\n    $v-&gt;save();\n}\n</code></pre>\n<p>Also avoid: assuming parent stock applies to variation-managed stock, and querying child variations via <code>WP_Query</code> / <code>get_posts</code> in hot paths instead of <code>$variable-&gt;get_children()</code>.</p>\n<h2>Cross-references</h2>\n<ul>\n<li>Run <strong><code>wc-variations-pricing-filters</code></strong> for the price filter chain (<code>woocommerce_product_variation_get_price</code>, <code>woocommerce_variation_prices_price</code>, etc.) — when a plugin needs to mutate variation prices via filters rather than direct CRUD.</li>\n<li>Run <strong><code>wc-variation-gallery</code></strong> for WooCommerce 10.9+ native variation gallery data (<code>gallery_image_ids</code>, <code>gallery_images_html</code>, REST v3 gallery payloads, and Additional Variation Images migration).</li>\n<li>Run <strong><code>wc-product-search-select</code></strong> when the UI needs an admin product picker — <code>woocommerce_json_search_products_and_variations</code> returns variation IDs alongside parent products.</li>\n<li>Run <strong><code>wp-plugin-cron</code></strong> for batch imports — cron callbacks scheduled idempotently are the right place for bulk variation operations.</li>\n</ul>\n<h2>What this skill does NOT cover</h2>\n<ul>\n<li>The price filter chain. Mutating variation prices via filters (without changing stored data) is a separate topic — see <code>wc-variations-pricing-filters</code>.</li>\n<li>The frontend variation switching UX (<code>found_variation</code> JS event, AJAX swatch / dropdown logic). That's frontend territory; this skill is data-layer.</li>\n<li>Variation image handling beyond <code>set_image_id()</code>. Native variation galleries are covered by <code>wc-variation-gallery</code>.</li>\n<li>Grouped products and external products — different post types, different rules.</li>\n<li>Extension-defined product types and their pricing rules.</li>\n<li>Product attributes registration / management — orthogonal topic; variations consume already-existing attributes.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li><code>WC_Product_Variable::get_variation_prices</code> and the cache: <a href=\"class-wc-product-variable.php\">wp-content/plugins/woocommerce/includes/class-wc-product-variable.php:99</a>.</li>\n<li><code>WC_Product_Variable::sync()</code> (static): <a href=\"class-wc-product-variable.php\">wp-content/plugins/woocommerce/includes/class-wc-product-variable.php</a>.</li>\n<li>Variation data store, transient name <code>wc_var_prices_</code>, price hash inputs: <a href=\"class-wc-product-variable-data-store-cpt.php\">wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php</a>.</li>\n<li><code>WC_Product_Variation</code>: <a href=\"class-wc-product-variation.php\">wp-content/plugins/woocommerce/includes/class-wc-product-variation.php</a>.</li>\n<li><code>wc_delete_product_transients()</code> for cache invalidation: <a href=\"wc-product-functions.php\">wp-content/plugins/woocommerce/includes/wc-product-functions.php</a>.</li>\n<li>Official documentation: <a href=\"https://github.com/woocommerce/woocommerce/wiki/Product-Variations\">https://github.com/woocommerce/woocommerce/wiki/Product-Variations</a></li>\n<li>Official documentation: <a href=\"https://woocommerce.com/document/managing-product-variations/\">https://woocommerce.com/document/managing-product-variations/</a></li>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-variation-data-store-cpt.php</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":17445,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:33.993976Z","sha256":"EE4B5FE465AA06A963F73D827BFE57841C53BCF02F0CAA58D576D16A3B43DD19","sizeBytes":6473},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"woocommerce/wc-variations-data","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"365DD2E3F56CF518EBEB5CC01032F14B3D07331DDB300944F1ED30A963AEE313","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:18:49.148964Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-variations-data"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}