WooCommerce stores product data across three database tables — wp_posts (the product post), wp_postmeta (all product attributes), and wp_term_relationships (categories, tags, and attributes). Reading product data by accessing these tables directly with get_post_meta() works, but it bypasses WooCommerce’s data abstraction layer, misses the in-memory object cache, and silently breaks when WooCommerce migrates data to custom tables (as in WooCommerce 8+ HPOS). The correct API is wc_get_product(), which returns a WC_Product object (or a subclass like WC_Product_Variable) with typed getter methods for every piece of product data. Inside the WooCommerce product loop, $product is already set to the current product — wc_get_product() is needed outside the loop or when accessing a specific product by ID.
Problem: A plugin needs to read a product's price, SKU, stock status, categories, and whether it is on sale — from outside the WooCommerce loop, given a product post ID from a custom query.
Solution: Call wc_get_product( $post_id ) to get a WC_Product object, then use its getter methods. Check instanceof WC_Product_Variable for variable products that need variation-level data.
<?php
$post_id = 123;
$product = wc_get_product( $post_id ); // returns WC_Product or false
if ( ! $product instanceof \WC_Product ) {
return; // not a valid product
}
// ── Basic product data ─────────────────────────────────────────────────
echo esc_html( $product->get_name() ); // product title
echo esc_html( $product->get_sku() ); // SKU
echo esc_html( $product->get_type() ); // 'simple', 'variable', 'grouped', 'external'
echo $product->get_id(); // post ID
echo esc_html( $product->get_description() ); // full description
echo esc_html( $product->get_short_description() );
// ── Pricing ────────────────────────────────────────────────────────────
echo wc_price( $product->get_price() ); // formatted with currency symbol
echo esc_html( $product->get_regular_price() ); // raw string e.g. "29.99"
echo esc_html( $product->get_sale_price() ); // empty string if not on sale
$product->is_on_sale(); // bool
$product->is_purchasable(); // bool (in stock + purchasable)
// ── Stock / inventory ──────────────────────────────────────────────────
$product->is_in_stock(); // bool
$product->get_stock_status(); // 'instock', 'outofstock', 'onbackorder'
$product->get_stock_quantity(); // int or null if not tracked
$product->managing_stock(); // bool
// ── Images ────────────────────────────────────────────────────────────
$image_id = $product->get_image_id();
$gallery_ids = $product->get_gallery_image_ids(); // array of attachment IDs
echo wp_get_attachment_image( $image_id, 'woocommerce_thumbnail' );
// ── Categories and tags ────────────────────────────────────────────────
$cat_ids = $product->get_category_ids(); // array of term IDs
$tag_ids = $product->get_tag_ids();
// ── Variable product: get all variations ──────────────────────────────
if ( $product instanceof \WC_Product_Variable ) {
$variation_ids = $product->get_children(); // array of variation post IDs
foreach ( $variation_ids as $vid ) {
$variation = wc_get_product( $vid );
if ( $variation instanceof \WC_Product_Variation ) {
echo esc_html( $variation->get_sku() );
echo wc_price( $variation->get_price() );
print_r( $variation->get_attributes() ); // [ 'pa_color' => 'red', ... ]
}
}
// Get cheapest and most expensive variation price
echo wc_price( $product->get_variation_price( 'min' ) );
echo wc_price( $product->get_variation_price( 'max' ) );
}
// ── Programmatically update product data ──────────────────────────────
$product->set_regular_price( '39.99' );
$product->set_sale_price( '29.99' );
$product->set_stock_status( 'instock' );
$product->set_manage_stock( true );
$product->set_stock_quantity( 50 );
$product->save(); // persist all changes to the database
NOTE: Never update WooCommerce product pricing by calling update_post_meta( $id, '_price', '29.99' ) directly — WooCommerce maintains three separate price fields (_price, _regular_price, _sale_price) and the _price field is a computed value that the product object updates automatically based on sale status and schedule. Bypassing the object's setters leaves _price out of sync with the sale price, breaking sorting and filtering by price. Always use $product->set_regular_price(), $product->set_sale_price(), and $product->save().