WooCommerce Development Patterns
This skill covers WooCommerce development including CRUD objects, hooks, custom product types, checkout customization, payment gateways, shipping methods, and HPOS compatibility.
HPOS Compatibility
WooCommerce High-Performance Order Storage (HPOS) replaces the wp_posts table for orders. Always use CRUD methods instead of post functions:
// CORRECT (HPOS-compatible):
$order = wc_get_order( $order_id );
$order->get_meta( '_custom_field' );
$order->update_meta_data( '_custom_field', $value );
$order->save();
// WRONG (breaks with HPOS):
$order = get_post( $order_id );
get_post_meta( $order_id, '_custom_field', true );
update_post_meta( $order_id, '_custom_field', $value );
Declare HPOS compatibility in your plugin:
add_action( 'before_woocommerce_init', function (): void {
if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
} );
WooCommerce CRUD Objects
WC_Product
// Get a product:
$product = wc_get_product( $product_id );
// Product properties:
$product->get_name();
$product->get_price();
$product->get_regular_price();
$product->get_sale_price();
$product->get_sku();
$product->get_stock_quantity();
$product->is_in_stock();
$product->is_on_sale();
$product->get_type(); // 'simple', 'variable', 'grouped', 'external'
$product->get_category_ids();
$product->get_image_id();
// Create a product:
$product = new WC_Product_Simple();
$product->set_name( 'My Product' );
$product->set_regular_price( '29.99' );
$product->set_description( 'Product description.' );
$product->set_short_description( 'Short description.' );
$product->set_sku( 'MY-SKU-001' );
$product->set_manage_stock( true );
$product->set_stock_quantity( 100 );
$product->set_category_ids( array( 15, 22 ) );
$product->save();
$new_id = $product->get_id();
WC_Order
// Get an order:
$order = wc_get_order( $order_id );
// Order properties:
$order->get_id();
$order->get_status();
$order->get_total();
$order->get_subtotal();
$order->get_billing_email();
$order->get_billing_first_name();
$order->get_shipping_address_1();
$order->get_payment_method();
$order->get_items();
$order->get_customer_id();
$order->get_date_created();
// Order items:
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$quantity = $item->get_quantity();
$subtotal = $item->get_subtotal();
$item_meta = $item->get_meta( '_custom_key' );
}
// Update order:
$order->set_status( 'completed' );
$order->add_order_note( __( 'Order processed.', 'myplugin' ) );
$order->update_meta_data( '_myplugin_processed', true );
$order->save();
// Create an order programmatically:
$order = wc_create_order( array( 'customer_id' => $user_id ) );
$order->add_product( wc_get_product( $product_id ), 2 );
$order->set_address( $billing_address, 'billing' );
$order->calculate_totals();
$order->save();
WC_Customer
$customer = new WC_Customer( $user_id );
$customer->get_billing_email();
$customer->get_billing_first_name();
$customer->get_order_count();
$customer->get_total_spent();
Key Hooks
Cart Hooks
// Modify cart item data:
add_filter( 'woocommerce_add_cart_item_data', function ( array $cart_item_data, int $product_id, int $variation_id ): array {
if ( isset( $_POST['custom_option'] ) ) {
$cart_item_data['custom_option'] = sanitize_text_field( wp_unslash( $_POST['custom_option'] ) );
}
return $cart_item_data;
}, 10, 3 );
// Validate before add to cart:
add_filter( 'woocommerce_add_to_cart_validation', function ( bool $passed, int $product_id, int $quantity ): bool {
if ( $quantity > 10 ) {
wc_add_notice( __( 'Maximum 10 items allowed.', 'myplugin' ), 'error' );
return false;
}
return $passed;
}, 10, 3 );
// Modify cart item price:
add_action( 'woocommerce_before_calculate_totals', function ( WC_Cart $cart ): void {
foreach ( $cart->get_cart() as $cart_item ) {
if ( isset( $cart_item['custom_price'] ) ) {
$cart_item['data']->set_price( $cart_item['custom_price'] );
}
}
} );
Checkout Hooks
// Add custom checkout field:
add_action( 'woocommerce_after_order_notes', function ( WC_Checkout $checkout ): void {
woocommerce_form_field( 'delivery_notes', array(
'type' => 'textarea',
'class' => array( 'form-row-wide' ),
'label' => __( 'Delivery Notes', 'myplugin' ),
'placeholder' => __( 'Special instructions...', 'myplugin' ),
'required' => false,
), $checkout->get_value( 'delivery_notes' ) );
} );
// Validate custom field:
add_action( 'woocommerce_checkout_process', function (): void {
// Validation logic here.
} );
// Save custom field:
add_action( 'woocommerce_checkout_create_order', function ( WC_Order $order, array $data ): void {
if ( ! empty( $_POST['delivery_notes'] ) ) {
$order->update_meta_data(
'_delivery_notes',
sanitize_textarea_field( wp_unslash( $_POST['delivery_notes'] ) )
);
}
}, 10, 2 );
Order Status Hooks
// When order status changes:
add_action( 'woocommerce_order_status_changed', function ( int $order_id, string $old_status, string $new_status, WC_Order $order ): void {
if ( 'completed' === $new_status ) {
// Handle completion.
}
}, 10, 4 );
// Specific status transition:
add_action( 'woocommerce_order_status_processing_to_completed', function ( int $order_id, WC_Order $order ): void {
// Runs only on processing → completed.
}, 10, 2 );
// After payment complete:
add_action( 'woocommerce_payment_complete', function ( int $order_id ): void {
$order = wc_get_order( $order_id );
// Post-payment logic.
} );
Custom Product Data Tab
// Add tab:
add_filter( 'woocommerce_product_data_tabs', function ( array $tabs ): array {
$tabs['myplugin_custom'] = array(
'label' => __( 'Custom Data', 'myplugin' ),
'target' => 'myplugin_custom_product_data',
'class' => array(),
'priority' => 70,
);
return $tabs;
} );
// Tab content:
add_action( 'woocommerce_product_data_panels', function (): void {
global $post;
?>
<div id="myplugin_custom_product_data" class="panel woocommerce_options_panel">
<?php
woocommerce_wp_text_input( array(
'id' => '_myplugin_custom_field',
'label' => __( 'Custom Field', 'myplugin' ),
'desc_tip' => true,
'description' => __( 'Enter a custom value.', 'myplugin' ),
) );
woocommerce_wp_select( array(
'id' => '_myplugin_option',
'label' => __( 'Option', 'myplugin' ),
'options' => array(
'' => __( 'Select...', 'myplugin' ),
'opt_a' => __( 'Option A', 'myplugin' ),
'opt_b' => __( 'Option B', 'myplugin' ),
),
) );
woocommerce_wp_checkbox( array(
'id' => '_myplugin_enabled',
'label' => __( 'Enable Feature', 'myplugin' ),
) );
?>
</div>
<?php
} );
// Save tab data:
add_action( 'woocommerce_process_product_meta', function ( int $post_id ): void {
$product = wc_get_product( $post_id );
if ( isset( $_POST['_myplugin_custom_field'] ) ) {
$product->update_meta_data( '_myplugin_custom_field',
sanitize_text_field( wp_unslash( $_POST['_myplugin_custom_field'] ) ) );
}
if ( isset( $_POST['_myplugin_option'] ) ) {
$product->update_meta_data( '_myplugin_option',
sanitize_key( $_POST['_myplugin_option'] ) );
}
$product->update_meta_data( '_myplugin_enabled',
isset( $_POST['_myplugin_enabled'] ) ? 'yes' : 'no' );
$product->save();
} );
Payment Gateway Skeleton
class WC_Gateway_MyPlugin extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'myplugin_gateway';
$this->method_title = __( 'My Payment Gateway', 'myplugin' );
$this->method_description = __( 'Accept payments via My Gateway.', 'myplugin' );
$this->has_fields = true;
$this->supports = array( 'products', 'refunds' );
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
$this->enabled = $this->get_option( 'enabled' );
add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
}
public function init_form_fields(): void {
$this->form_fields = array(
'enabled' => array(
'title' => __( 'Enable/Disable', 'myplugin' ),
'type' => 'checkbox',
'label' => __( 'Enable My Gateway', 'myplugin' ),
'default' => 'no',
),
'title' => array(
'title' => __( 'Title', 'myplugin' ),
'type' => 'text',
'default' => __( 'My Gateway', 'myplugin' ),
),
'api_key' => array(
'title' => __( 'API Key', 'myplugin' ),
'type' => 'password',
),
);
}
public function process_payment( $order_id ): array {
$order = wc_get_order( $order_id );
// Process payment with external API...
$order->payment_complete( $transaction_id );
$order->add_order_note( sprintf(
/* translators: %s: transaction ID */
__( 'Payment completed. Transaction ID: %s', 'myplugin' ),
$transaction_id
) );
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $order ),
);
}
public function process_refund( $order_id, $amount = null, $reason = '' ): bool {
$order = wc_get_order( $order_id );
// Process refund with external API...
return true; // or WP_Error
}
}
// Register:
add_filter( 'woocommerce_payment_gateways', function ( array $gateways ): array {
$gateways[] = 'WC_Gateway_MyPlugin';
return $gateways;
} );
Shipping Method Skeleton
class WC_Shipping_MyPlugin extends WC_Shipping_Method {
public function __construct( $instance_id = 0 ) {
$this->id = 'myplugin_shipping';
$this->instance_id = absint( $instance_id );
$this->method_title = __( 'My Shipping', 'myplugin' );
$this->method_description = __( 'Custom shipping method.', 'myplugin' );
$this->supports = array( 'shipping-zones', 'instance-settings' );
$this->init();
}
public function init(): void {
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option( 'title' );
add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
}
public function calculate_shipping( $package = [] ): void {
$rate = array(
'id' => $this->get_rate_id(),
'label' => $this->title,
'cost' => '10.00',
);
$this->add_rate( $rate );
}
}
add_action( 'woocommerce_shipping_init', function (): void {
// Class defined above or included here.
} );
add_filter( 'woocommerce_shipping_methods', function ( array $methods ): array {
$methods['myplugin_shipping'] = 'WC_Shipping_MyPlugin';
return $methods;
} );
WooCommerce REST API Extensions
add_action( 'rest_api_init', function (): void {
register_rest_route( 'myplugin/v1', '/orders/(?P<id>\d+)/custom-data', array(
'methods' => 'GET',
'callback' => function ( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$order = wc_get_order( $request['id'] );
if ( ! $order ) {
return new WP_Error( 'not_found', __( 'Order not found', 'myplugin' ), array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'custom_field' => $order->get_meta( '_myplugin_custom_field' ),
) );
},
'permission_callback' => function (): bool {
return current_user_can( 'manage_woocommerce' );
},
) );
} );
// Extend existing WC REST API responses:
add_filter( 'woocommerce_rest_prepare_product_object', function ( WP_REST_Response $response, WC_Product $product, WP_REST_Request $request ): WP_REST_Response {
$response->data['myplugin_custom'] = $product->get_meta( '_myplugin_custom_field' );
return $response;
}, 10, 3 );
WooCommerce Email Customization
class WC_Email_MyPlugin_Notification extends WC_Email {
public function __construct() {
$this->id = 'myplugin_notification';
$this->title = __( 'Custom Notification', 'myplugin' );
$this->description = __( 'Sent when a custom event occurs.', 'myplugin' );
$this->heading = __( 'Custom Notification', 'myplugin' );
$this->subject = __( '[{site_title}]: Custom Notification', 'myplugin' );
$this->template_base = MYPLUGIN_PATH . 'templates/';
$this->template_html = 'emails/custom-notification.php';
$this->template_plain = 'emails/plain/custom-notification.php';
$this->customer_email = true;
parent::__construct();
}
public function trigger( int $order_id ): void {
$this->setup_locale();
$order = wc_get_order( $order_id );
if ( $order ) {
$this->object = $order;
$this->recipient = $order->get_billing_email();
$this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
}
$this->restore_locale();
}
public function get_content_html(): string {
return wc_get_template_html( $this->template_html, array(
'order' => $this->object,
'email_heading' => $this->get_heading(),
'email' => $this,
), '', $this->template_base );
}
}
add_filter( 'woocommerce_email_classes', function ( array $emails ): array {
$emails['WC_Email_MyPlugin_Notification'] = new WC_Email_MyPlugin_Notification();
return $emails;
} );
For the complete WooCommerce hook reference, see references/woo-hooks.md.