Skip to content
ThemesIonic — home
WooCommerce

Changing Product Prices with woocommerce_product_get_price

The filter changes the price the cart charges, not just the one shown, but not for variations. Variations have their own filter, the variable price range has a third with a cache, and catalogue sorting ignores all of them.

5 min read advanced

woocommerce_product_get_price filters the value $product->get_price() returns, so it changes the price WooCommerce charges, not only the one it displays. It receives two arguments, the price as stored and the product object, and it runs every time code asks for the price in the default view context. The cart builds its totals from get_price() on each line's product, so a filtered price ends up on the order. That is the main difference from woocommerce_get_price_html, which only changes the markup.

The name is built, not declared. Product getters call get_prop(), which applies woocommerce_product_get_{prop}. That is why there is also woocommerce_product_get_regular_price, woocommerce_product_get_sale_price, and the trap below.

The working shape

add_filter( 'woocommerce_product_get_price', 'ti_member_price', 10, 2 );
add_filter( 'woocommerce_product_variation_get_price', 'ti_member_price', 10, 2 );

function ti_member_price( $price, $product ) {
	if ( '' === $price || ! is_user_logged_in() ) {
		return $price;
	}

	if ( ! wc_current_user_has_role( 'member' ) ) {
		return $price;
	}

	// Start from the stored regular price, never from get_price().
	$regular = (float) $product->get_regular_price( 'edit' );

	$member  = round( $regular * 0.9, wc_get_price_decimals() );

	// A sale price lower than the member price still wins.
	return (string) min( $member, (float) $price );
}

Four details in it are deliberate:

  • The empty string passes through. A product with no price returns '', and is_purchasable() treats a '' price as not for sale. Turning that into 0 makes unpriced products buyable for free.
  • No call to $product->get_price() inside the callback. It runs the filter again and loops until PHP runs out of memory. Use the $price argument or the edit context, which skips filters.
  • The base is the regular price. If other code has called set_price() on the cart's copy of the product, for example in woocommerce_before_calculate_totals, $price already contains that change. Multiplying $price would stack your discount on top of it.
  • min() against the current price. Without it, a member pays 90 % of the regular price while a guest pays a deeper sale price.

The trap: variable products need three filters

A variable product's page shows a price range, such as "£20 – £30", and that range ignores both filters above. It is built by the variable product data store from each variation's price in the edit context, passed through a separate filter, and cached:

What the shopper sees Where the number comes from Filter
Simple product price, cart line, order get_price() woocommerce_product_get_price
Selected variation's price, cart line, order get_price() on the variation woocommerce_product_variation_get_price
Price range on a variable product Cached variation prices woocommerce_variation_prices_price (plus _regular_price, _sale_price)

Hook only the first two and the catalogue keeps advertising the old range while the selected variation and the cart show the new price. Add the range filters with the same logic:

add_filter( 'woocommerce_variation_prices_price', 'ti_member_range_price', 10, 3 );

function ti_member_range_price( $price, $variation, $product ) {
	return ti_member_price( $price, $variation );
}

Then deal with the cache. The computed prices are stored in a transient named wc_var_prices_{product_id}, under a hash built from the tax setup and the callbacks attached to the three woocommerce_variation_prices_* filters. The hash records which callbacks are attached, not who is looking. A price that depends on the user is cached for the first visitor, then served to everyone. Add the deciding factor to the hash:

add_filter( 'woocommerce_get_variation_prices_hash', function ( $hash, $product, $for_display ) {
	$hash[] = wc_current_user_has_role( 'member' ) ? 'member' : 'guest';
	return $hash;
}, 10, 3 );

Use a group key like this, not the user ID, unless prices really are per user. Each distinct key adds an entry to the product's transient.

What the filter does reach

Because the value comes from the product object, the filtered price travels further than display code:

  • Coupons calculate percentage discounts from the cart line, which is built from the filtered price, so a 10 % coupon on a member price takes 10 % of the member price.
  • The REST API and the Store API read get_price() in the view context. Apps, feeds and the block cart all receive the filtered number; only a REST request with context=edit gets the stored price.
  • Tax is calculated on the filtered price, inclusive or exclusive, exactly as on a stored price.

What the filter does not reach

  • Sorting by price and the price filter. Both query the wc_product_meta_lookup table, filled from the stored _price meta. A filtered price does not move a product in "Sort by price" or out of a price range. If the difference matters, the price has to be stored, not filtered. Filter not working covers the lookup tables and where to regenerate them.
  • The sale badge. is_on_sale() compares the regular and sale prices, not get_price(). A filtered lower price shows without a strikethrough or a Sale flash unless you also filter the sale price.
  • Page caches. A cached catalogue page shows whatever price the first visitor saw. User-dependent prices need the cache to bypass logged-in users or that customer group.
  • Existing orders. They keep the totals they were created with.

Performance

get_price() is called many times per product on one page, for display, tax calculation, the cart and the mini cart. Keep the callback cheap: no database queries per call, and no remote lookups. Read anything expensive once per request and keep it in a static variable. For shops with many pricing rules, the trade-offs between filters and stored prices are covered in B2B and wholesale.

Verify

  1. As a member and as a guest, compare a simple product's price on the product page, in the cart and on a test order.
  2. On a variable product, compare the range, the selected variation's price and the cart line. All three should agree.
  3. Load the variable product as a guest after loading it as a member. The range must not carry over.
  4. Sort the catalogue by price and confirm you are not expecting the filtered price to change the order.

Frequently asked

Variations are WC_Product_Variation objects, whose getters use the prefix woocommerce_product_variation_get_. Their price goes through woocommerce_product_variation_get_price, not woocommerce_product_get_price. Hook the same callback to both.
This filter changes the price everywhere the product is read, including the catalogue, so shoppers see the price they will pay. woocommerce_before_calculate_totals changes only cart lines. Use the filter for prices that belong to the product or customer, and the cart hook for rules that depend on the cart, such as quantity breaks.
No. Order line items store their totals when the order is created, and they are not recalculated from the product. Orders created from the admin do read the product's price when you add an item, so a filter that only makes sense for shoppers needs a guard there.

Related guides