WooCommerce Product Bundles: Building a Custom Bundle with Meta and Pricing

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.