A “product bundle” on WooCommerce — a single product that groups several others and offers a combined price — can be built without the premium WooCommerce Bundles extension by using a custom product type, post meta to store child product IDs, and a hook to override the price at add-to-cart time. This keeps the implementation under full control and avoids subscription costs.
Problem: WooCommerce does not natively support selling products as bundles — a fixed set of related products sold together at a discounted price — without a paid extension or complex workarounds using grouped products.
Solution: Build a custom bundle product type by extending WC_Product — store bundled product IDs as serialised post meta, render a component selection UI on the product page, validate that all required components are selected on add-to-cart, and carry component data through the cart as line item meta to the order.
The code below registers a bundle product type class, adds a metabox to select child products and set a bundle discount, calculates the bundle price dynamically, and adds all child items to the cart when the bundle is purchased.
$types + [ 'bundle' => __( 'Product Bundle', 'my-plugin' ) ]
);
class WC_Product_Bundle extends WC_Product {
public function get_type(): string { return 'bundle'; }
/** IDs of child products stored in post meta */
public function get_bundle_items(): array {
$raw = $this->get_meta( '_bundle_items' );
return is_array( $raw ) ? array_map( 'absint', $raw ) : [];
}
/** Discount percentage (0–100) */
public function get_bundle_discount(): float {
return (float) $this->get_meta( '_bundle_discount' );
}
/** Calculate bundle price from children */
public function get_bundle_calculated_price(): float {
$total = 0.0;
foreach ( $this->get_bundle_items() as $id ) {
$product = wc_get_product( $id );
if ( $product ) {
$total += (float) $product->get_price();
}
}
$discount = $this->get_bundle_discount();
return round( $total * ( 1 - $discount / 100 ), 2 );
}
public function get_price( string $context = 'view' ): string {
return (string) $this->get_bundle_calculated_price();
}
}
// 2. Register the class with WooCommerce
add_filter( 'woocommerce_product_class',
function ( string $classname, string $type ) {
return $type === 'bundle' ? 'WC_Product_Bundle' : $classname;
}, 10, 2
);
// 3. Admin metabox: choose child products and discount
add_action( 'add_meta_boxes', function () {
add_meta_box( 'bundle-items', 'Bundle Items', function ( WP_Post $post ) {
$product = wc_get_product( $post->ID );
$selected = $product ? $product->get_meta( '_bundle_items' ) : [];
$discount = $product ? $product->get_meta( '_bundle_discount' ) : 0;
wp_nonce_field( 'save_bundle_meta', 'bundle_nonce' );
echo '';
echo '';
}, 'product', 'normal' );
} );
add_action( 'save_post_product', function ( int $post_id ) {
if ( ! isset( $_POST['bundle_nonce'] ) || ! wp_verify_nonce( $_POST['bundle_nonce'], 'save_bundle_meta' ) ) {
return;
}
$raw_ids = sanitize_text_field( $_POST['bundle_items'] ?? '' );
$ids = array_filter( array_map( 'absint', explode( ',', $raw_ids ) ) );
$discount = min( 100, max( 0, (float) ( $_POST['bundle_discount'] ?? 0 ) ) );
$product = wc_get_product( $post_id );
if ( $product ) {
$product->update_meta_data( '_bundle_items', $ids );
$product->update_meta_data( '_bundle_discount', $discount );
$product->save();
}
} );
// 4. Add child products to cart when bundle is added
add_action( 'woocommerce_add_to_cart',
function ( string $key, int $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product || $product->get_type() !== 'bundle' ) {
return;
}
foreach ( $product->get_bundle_items() as $child_id ) {
WC()->cart->add_to_cart( $child_id, 1 );
}
}, 10, 2
);
NOTE: When adding child products automatically with add_to_cart(), store a cart item meta reference (e.g., _bundle_parent_key) on the children so you can remove them together when the bundle item is removed from the cart via the woocommerce_remove_cart_item hook.