WooCommerce classic cart and checkout
Use this for the PHP/classic cart and shortcode checkout flow. It covers product add-to-cart customization, cart item data, calculated prices/fees, checkout fields, checkout validation, and copying cart data to orders.
It is not a Checkout Block UI skill. Some low-level cart hooks also run during Store API requests, but woocommerce_checkout_fields does not make fields appear in the Checkout Block. For block/headless cart state, use wc-store-api.
Misconception this skill corrects
"I added custom cart item data, so WooCommerce will automatically show it on the order."
Cart item data affects the cart key and lives in the cart/session. It is not automatically saved as order line-item meta. Display it with woocommerce_get_item_data, and copy it to the order line with woocommerce_checkout_create_order_line_item.
When to use this skill
Trigger when ANY of the following is true:
- Adding a product option from an add-to-cart form.
- Storing custom data on a cart item.
- Showing custom data in cart/checkout/order line items.
- Changing cart item price dynamically.
- Adding a handling, insurance, gift-wrap, or payment-related fee.
- Adding fields to classic checkout.
- Validating checkout data server-side.
- The diff contains
woocommerce_add_cart_item_data,woocommerce_get_item_data,woocommerce_before_calculate_totals,woocommerce_cart_calculate_fees,woocommerce_checkout_fields,woocommerce_after_checkout_validation, orwoocommerce_checkout_create_order_line_item.
Cart item identity
WC_Cart::add_to_cart() applies woocommerce_add_cart_item_data before generating the cart ID. WC_Cart::generate_cart_id() includes product ID, variation ID, variation attributes, and every value in $cart_item_data.
That means:
- If two cart additions have the same product, variation, and cart item data, Woo merges quantities into one cart line.
- If cart item data differs, Woo creates a different cart line.
- Do not add a random unique value unless you intentionally want every add-to-cart click to be a separate line.
add_filter(
'woocommerce_add_cart_item_data',
static function ( array $cart_item_data, int $product_id, int $variation_id, int $quantity ): array {
if ( empty( $_POST['myplugin_engraving'] ) ) {
return $cart_item_data;
}
$engraving = sanitize_text_field( wp_unslash( $_POST['myplugin_engraving'] ) );
if ( '' === $engraving ) {
return $cart_item_data;
}
$cart_item_data['myplugin_engraving'] = $engraving;
// Only add this if identical configured items must never merge:
// $cart_item_data['myplugin_line_uid'] = wp_generate_uuid4();
return $cart_item_data;
},
10,
4
);
The classic add-to-cart form path also applies woocommerce_add_to_cart_validation. Use it to reject invalid posted product options before the cart line is created.
Display cart item data
woocommerce_get_item_data feeds wc_get_formatted_cart_item_data(), which is used by cart and checkout templates.
add_filter(
'woocommerce_get_item_data',
static function ( array $item_data, array $cart_item ): array {
if ( empty( $cart_item['myplugin_engraving'] ) ) {
return $item_data;
}
$item_data[] = array(
'name' => __( 'Engraving', 'myplugin' ),
'value' => esc_html( $cart_item['myplugin_engraving'] ),
);
return $item_data;
},
10,
2
);
This is display only. It does not persist to the order.
Copy cart data to order lines
Use woocommerce_checkout_create_order_line_item for per-item meta. Do not put line-item data into woocommerce_checkout_update_order_meta; that hook is order-level.
add_action(
'woocommerce_checkout_create_order_line_item',
static function ( WC_Order_Item_Product $item, string $cart_item_key, array $values, WC_Order $order ): void {
if ( empty( $values['myplugin_engraving'] ) ) {
return;
}
$item->add_meta_data(
__( 'Engraving', 'myplugin' ),
sanitize_text_field( $values['myplugin_engraving'] ),
true
);
},
10,
4
);
Use a private meta key such as _myplugin_config if the value is for machine logic only. Use a readable label when the value should appear to admins/customers.
Dynamic cart item prices
woocommerce_before_calculate_totals runs whenever Woo recalculates totals. Set an absolute price every time; do not add to the current price repeatedly.
Store the base value when the cart item is created:
add_filter(
'woocommerce_add_cart_item_data',
static function ( array $cart_item_data, int $product_id, int $variation_id ): array {
$product = wc_get_product( $variation_id ?: $product_id );
if ( $product instanceof WC_Product ) {
$cart_item_data['myplugin_base_price'] = (float) $product->get_price( 'edit' );
}
return $cart_item_data;
},
20,
3
);
Then set the calculated price:
add_action(
'woocommerce_before_calculate_totals',
static function ( WC_Cart $cart ): void {
if ( is_admin() && ! wp_doing_ajax() ) {
return;
}
foreach ( $cart->get_cart() as $cart_item ) {
if ( empty( $cart_item['myplugin_engraving'] ) || ! isset( $cart_item['myplugin_base_price'] ) ) {
continue;
}
$cart_item['data']->set_price( (float) $cart_item['myplugin_base_price'] + 5.00 );
}
},
20
);
Do not call update_post_meta() or product setters that save the product here. The product object in the cart line is a runtime object; the catalog product price should not be changed.
Fees
Use woocommerce_cart_calculate_fees for cart-level fees. Do not add fees in woocommerce_before_calculate_totals.
add_action(
'woocommerce_cart_calculate_fees',
static function ( WC_Cart $cart ): void {
if ( is_admin() && ! wp_doing_ajax() ) {
return;
}
if ( $cart->is_empty() ) {
return;
}
$cart->add_fee( __( 'Handling', 'myplugin' ), 5.00, true, '' );
}
);
Fees become WC_Order_Item_Fee items during checkout. They are not product line items.
Classic checkout fields
woocommerce_checkout_fields modifies the field arrays for classic checkout sections: billing, shipping, account, and order.
add_filter(
'woocommerce_checkout_fields',
static function ( array $fields ): array {
$fields['billing']['billing_vat_id'] = array(
'type' => 'text',
'label' => __( 'VAT ID', 'myplugin' ),
'required' => false,
'priority' => 120,
'autocomplete'=> 'off',
);
return $fields;
}
);
add_action(
'woocommerce_after_checkout_validation',
static function ( array $data, WP_Error $errors ): void {
if ( empty( $data['billing_vat_id'] ) ) {
return;
}
if ( ! preg_match( '/^[A-Z0-9 -]{4,32}$/i', (string) $data['billing_vat_id'] ) ) {
$errors->add( 'billing_vat_id', __( 'Enter a valid VAT ID.', 'myplugin' ) );
}
},
10,
2
);
add_action(
'woocommerce_checkout_update_order_meta',
static function ( int $order_id, array $data ): void {
if ( empty( $data['billing_vat_id'] ) ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order instanceof WC_Order ) {
return;
}
$order->update_meta_data( '_billing_vat_id', sanitize_text_field( $data['billing_vat_id'] ) );
$order->save();
},
10,
2
);
Use Woo order APIs for HPOS compatibility. Never write checkout order data with update_post_meta( $order_id, ... ).
Common mistakes
- Adding random cart item data unintentionally prevents quantity merging.
- Changing
$product->set_price( $product->get_price() + 5 )in every totals calculation compounds the price. - Saving product objects from cart hooks changes catalog data.
- Adding fees from
woocommerce_before_calculate_totalsinstead ofwoocommerce_cart_calculate_fees. - Expecting
woocommerce_checkout_fieldsto render in Checkout Block. - Saving line-item data in order meta instead of
woocommerce_checkout_create_order_line_item. - Trusting posted product/checkout fields without sanitizing and validating.
- Using
$_SESSION; useWC()->sessionfor cart/session state.
Cross-skill routing
- Checkout Block, Store API cart, headless checkout:
wc-store-api - HPOS order storage concerns:
wc-hpos-compatibility - Customer/session persistence:
wc-customer-and-sessions - Payment gateway checkout processing:
wc-payment-gateway