Extending WooCommerce: Hooks & Custom Functionality

WordPress E-Commerce with WooCommerce

Chapter 8 · Extending WooCommerce: Hooks & Custom Functionality

Chapter 3 warned that copying a template file forks it, permanently losing that file's future WooCommerce updates — and pointed forward to hooks as the lower-risk alternative. This chapter is that payoff: WordPress Intermediate/Advanced 5's own actions-and-filters material, applied to WooCommerce's own large, separate set of hooks.

Actions vs. Filters, Recapped for WooCommerce

ActionsFilters
"Do something at this point" — no return value expected"Modify this value and return it" — the hook is useless if nothing is returned
add_action( 'hook_name', 'callback' )add_filter( 'hook_name', 'callback' )
e.g. sending a notification once an order reaches a given statuse.g. adjusting a cart item's price before totals are calculated

This is exactly the mechanism WordPress Intermediate/Advanced 5 already taught — nothing new conceptually. What's new is the specific set of hooks: WooCommerce fires hundreds of its own action and filter hooks throughout the cart, checkout, product display, and order lifecycle, entirely separate from WordPress core's own hooks.

Finding the Right Hook

WooCommerce publishes an official hook reference documenting its own actions and filters by name and by the data each one provides. In practice, confirming a hook actually fires when and how expected is often done directly and quickly — temporarily adding a logging line inside a candidate callback while testing is a normal, standard part of the workflow, not a sign of doing it wrong.

A Real Example: A Bulk-Discount Pricing Filter

A common, genuinely useful customization: automatically discounting a cart item once its quantity reaches a threshold. The standard, reliable pattern for adjusting cart-item prices hooks into woocommerce_before_calculate_totals — an action, despite modifying a price, because it acts on the whole cart object rather than returning a single filtered value.

<?php add_action( 'woocommerce_before_calculate_totals', 'apply_bulk_discount', 20, 1 ); function apply_bulk_discount( $cart ) { if ( is_admin() && ! defined( 'DOING_AJAX' ) ) { return; // don't run this on ordinary wp-admin page loads } foreach ( $cart->get_cart() as $cart_item ) { if ( $cart_item['quantity'] >= 5 ) { $original_price = $cart_item['data']->get_price(); $cart_item['data']->set_price( $original_price * 0.9 ); // 10% off } } } ?>

$cart_item['data'] is the product object for that line item — the same product object WordPress Intermediate/Advanced 4 already covered as a custom post type instance, reached here through the cart rather than a direct query. Calling set_price() on it changes the price used for totals, tax, and the order eventually created at checkout, without ever touching the product's own stored price in wp_postmeta.

The admin/AJAX guard at the top isn't optional
woocommerce_before_calculate_totals also fires on ordinary wp-admin page loads — without the guard shown above, this same code could run in contexts with no real cart to discount, or interfere with the admin order-editing screen. This is a small, real, commonly-forgotten detail specific to this exact hook.

A Real Example: An Action for a Side Effect

Not every customization changes a value — some just need to do something at a specific point. Notifying an internal fulfillment system once an order is marked complete is a genuine action, not a filter:

<?php add_action( 'woocommerce_order_status_completed', 'notify_fulfillment_team' ); function notify_fulfillment_team( $order_id ) { $order = wc_get_order( $order_id ); wp_mail( 'fulfillment@example.com', sprintf( 'Order #%d ready for fulfillment', $order_id ), $order->get_formatted_billing_full_name() ); } ?>

wc_get_order() returns a full order object — the same underlying order data Chapter 4 described being created at checkout, now available inside a hook triggered by that same order's own status change.

Where to Put This Code

WordPress Intermediate/Advanced 5 already made the case for a small custom plugin over a theme's own functions.php — a plugin survives a future theme change, functions.php doesn't. That reasoning applies here without modification.

A WooCommerce-specific timing gotcha, worth naming precisely
Code that references WooCommerce's own classes or functions must not run before WooCommerce itself has finished loading — a fatal error results if a class like WC_Cart is referenced before it exists yet. Wrapping WooCommerce-dependent code inside a plugins_loaded hook, or checking class_exists( 'WooCommerce' ) first, avoids this specific failure mode, which otherwise tends to appear intermittently depending on plugin load order.

Hands-On Exercises

Exercise 1

Explain why the bulk-discount example hooks into an action (woocommerce_before_calculate_totals) rather than a filter, even though its actual job is changing a price.

📄 View solution
Exercise 2

A developer removes the is_admin()/DOING_AJAX guard from the top of the bulk-discount function, believing it's unnecessary boilerplate. Explain what problem this chapter warns this could cause.

📄 View solution
Exercise 3

A developer's custom plugin throws a fatal error referencing an undefined WC_Cart class. Explain the likely cause, and the two fixes this chapter names for it.

📄 View solution

Chapter 8 Quick Reference

  • Same actions/filters mechanism as WordPress Intermediate/Advanced 5 — WooCommerce just fires its own large, separate set of hooks
  • woocommerce_before_calculate_totals + set_price() — the standard pattern for custom cart-item pricing (an action, since it acts on the whole cart object)
  • woocommerce_order_status_completed — a real example of an action used for a side effect (a notification), not a value change
  • Put custom hooks in a small site-specific plugin, not functions.php — the same guidance WordPress Intermediate/Advanced 5 already gave
  • Guard any WooCommerce-dependent code with class_exists( 'WooCommerce' ) or a plugins_loaded hook to avoid a fatal error from code running before WooCommerce itself has loaded
  • Next chapter: Performance at Scale