phpwoocommercebundleproductcart

Add specific product to cart together with the main product in WooCommerce


I try do something similar to 'product addons', but manually. I would have few simple products, that have to be as companions. Their ids will be the same. I want to make them as checkboxes, with photo, name and price. If checkbox is checked - that product has to add to cart together with main on the product page. That's a code, already tweaked by gpt

// Function to display checkboxes for additional products
function display_additional_products_checkboxes() {
    $additional_products_ids = array(220, 222, 224, 226); // Replace with your product IDs

    echo '<div class="additional-products">';
    echo '<h3>Additional Products</h3>';

    foreach ($additional_products_ids as $product_id) {
        $additional_product = wc_get_product($product_id);
        if ($additional_product) {
            $product_name = $additional_product->get_name();
            $product_price = $additional_product->get_price_html();
            $product_id_sanitized = sanitize_title($additional_product->get_slug());

            echo '<div class="additional-product">';
            echo '<label for="additional_product_' . $product_id_sanitized . '">';
            echo '<input type="checkbox" name="additional_products[]" id="additional_product_' . $product_id_sanitized . '" value="' . $product_id . '">';
            echo $product_name . ' - ' . $product_price;
            echo '</label>';
            echo '</div>';
        }
    }

    echo '</div>';
}
add_action('woocommerce_before_add_to_cart_button', 'display_additional_products_checkboxes');

// Function to process additional products checkboxes
function add_additional_products_to_cart($cart_item_data, $product_id, $variation_id) {
    if (isset($_POST['additional_products']) && !empty($_POST['additional_products'])) {
        $additional_products = wc_clean(wp_unslash($_POST['additional_products']));
        $cart_item_data['additional_products'] = $additional_products;
    }
    return $cart_item_data;
}
add_filter('woocommerce_add_cart_item_data', 'add_additional_products_to_cart', 10, 3);

// Function to display additional products in cart
function display_additional_products_in_cart($item_data, $cart_item) {
    if (isset($cart_item['additional_products']) && !empty($cart_item['additional_products'])) {
        $additional_products = $cart_item['additional_products'];
        $additional_products_names = array();

        foreach ($additional_products as $product_id) {
            $product = wc_get_product($product_id);
            if ($product) {
                $additional_products_names[] = $product->get_name();
            } else {
                error_log("Failed to get additional product with ID: $product_id");
            }
        }

        $item_data[] = array(
            'key'   => __('Additional Products', 'woocommerce'),
            'value' => implode(', ', $additional_products_names),
        );
    }

    return $item_data;
}
add_filter('woocommerce_get_item_data', 'display_additional_products_in_cart', 10, 2);

// Function to add additional products to order
function add_additional_products_to_order($item, $cart_item_key, $values, $order) {
    if (isset($values['additional_products']) && !empty($values['additional_products'])) {
        $additional_products = $values['additional_products'];

        foreach ($additional_products as $product_id) {
            $product = wc_get_product($product_id);
            if ($product) {
                $item->add_meta_data(__('Additional Product', 'woocommerce'), $product->get_name());
            } else {
                error_log("Failed to get additional product with ID: $product_id");
            }
        }
    }
}
add_action('woocommerce_checkout_create_order_line_item', 'add_additional_products_to_order', 10, 4);

// Function to add additional products as separate line items in the cart
function add_additional_products_as_separate_items($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
    if (isset($cart_item_data['additional_products']) && !empty($cart_item_data['additional_products'])) {
        if (!isset(WC()->session->additional_products_added)) {
            WC()->session->set('additional_products_added', array());
        }
        $processed = WC()->session->get('additional_products_added');

        $additional_products = $cart_item_data['additional_products'];

        foreach ($additional_products as $additional_product_id) {
            if (!in_array($additional_product_id, $processed)) {
                $additional_product = wc_get_product($additional_product_id);
                if ($additional_product) {
                    error_log("Adding additional product to cart: " . $additional_product->get_name());
                    WC()->cart->add_to_cart($additional_product_id);
                    $processed[] = $additional_product_id; // Mark this additional product as processed
                } else {
                    error_log("Failed to add additional product with ID: $additional_product_id");
                }
            }
        }

        WC()->session->set('additional_products_added', $processed);
    }
}
add_action('woocommerce_add_to_cart', 'add_additional_products_as_separate_items', 10, 6);

// Clear session data after checkout
function clear_additional_products_session() {
    if (isset(WC()->session->additional_products_added)) {
        WC()->session->__unset('additional_products_added');
    }
}
add_action('woocommerce_checkout_order_processed', 'clear_additional_products_session');

But I still have the same issue, and gpt can't help - I have it again and again. Log shows something like

[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One
[25-Jun-2024 04:21:09 UTC] Adding additional product to cart: Present One

Like endless loop. Gpt tried to help with session moment, but it didn't help. Maybe some human would know better where I'm wrong?


Solution

  • You are making things more complicated than they should be. You don't need to use WC Sessions, you don't need a function to display the additional products, as you are adding them to cart, and you don't need a function to add the additional products as order items.

    The main issue is that you are making an infinite loop when using woocommerce_add_to_cart:
    As woocommerce_add_to_cart hook is located in WC_Cart add_to_cart() method, when adding to cart the additional products, they trigger themselves woocommerce_add_to_cart again and again...

    Below I have added to WooCommerce Admin Product settings a multi-select field that will allow you to define the additional products at the product level:

    // Display a multi select field to Admin Product edit pages
    add_action('woocommerce_product_options_general_product_data', 'add_admin_product_custom_field');
    function add_admin_product_custom_field() {
        global $post, $product_object;
    
        $field_key = 'bundled_ids';
    
        printf('<div class="options_group"><p class="form-field"><label for="%s">%s</label>
        <select class="wc-product-search" multiple="multiple" style="%s" id="%s" name="%s[]" '.
        'data-placeholder="%s" data-action="woocommerce_json_search_products_and_variations" data-exclude="%d">',
        $field_key, esc_html__('Bundled products', 'woocommerce'), 'width: 50%;', $field_key, 
        $field_key, esc_attr__('Search for a product&hellip;', 'woocommerce'), intval($post->ID) );
    
        $product_ids = (array) $product_object->get_meta('_'.$field_key);
    
        foreach ( $product_ids as $product_id ) {
            $product = wc_get_product( $product_id );
            if ( is_object( $product ) ) {
                echo '<option value="' . esc_attr( $product_id ) . '"' . selected( true, true, false ) . '>' . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) ) . '</option>';
            }
        }
        
        echo '</select>' . wc_help_tip( __( 'Bundled products are products that will be added to cart optionally.', 'woocommerce' ) ); // WPCS: XSS ok.
        echo '</p></div>';
    }
    
    // Save custom field values from product edit page
    add_action('woocommerce_admin_process_product_object', 'save_admin_product_custom_field_value');
    function save_admin_product_custom_field_value( $product ) {
        $field_key = 'bundled_ids';
        $product->update_meta_data('_'.$field_key, isset($_POST[$field_key]) ? wc_clean( wp_unslash($_POST[$field_key]) ) : array());
    }
    

    You will get:

    enter image description here

    Then here is your revised code that display the additional products checkboxes and add to cart the selected additional products as separated cart items (commented):

    // Display checkbox(es) for additional product(s)
    add_action('woocommerce_before_add_to_cart_button', 'display_additional_products_checkboxes');
    function display_additional_products_checkboxes() {
        global $product;
    
        if ( $bundled_product_ids = $product->get_meta('_bundled_ids') ) {
            echo '<div class="additional-products">
            <h3>'. __('Additional Products') .'</h3>';
    
            foreach ($bundled_product_ids as $_product_id) {
                $_product = wc_get_product($_product_id);
    
                if ( is_object($_product) ) {
                    printf('<div class="form-row"><label for="bundled-products-%d">
                    <input type="checkbox" name="bundled_products[]" id="bundled-products-%d" value="%d">
                    &nbsp;%s: %s</label></div>', 
                    $_product_id, $_product_id, $_product_id, $_product->get_name(), $_product->get_price_html() );
                }
            }
    
            echo '</div><br>';
        }
    }
    
    // Add selected additional product IDs as custom cart item data
    add_filter( 'woocommerce_add_cart_item_data', 'set_bundled_products_as_custom_item_data', 10 );
    function set_bundled_products_as_custom_item_data( $cart_item_data ) {
        if ( ! isset($cart_item_data['parent_id']) 
        && isset($_POST['bundled_products']) && ! empty($_POST['bundled_products']) ) {
            $cart_item_data['bundled_products'] = wc_clean( wp_unslash($_POST['bundled_products']) );
        }
        return $cart_item_data;
    }
    
    // Add selected additional products as separated cart items
    add_action( 'template_redirect', 'add_to_cart_bundled_products' );
    function add_to_cart_bundled_products( $cart_item_data ) {
        if ( isset($_POST['bundled_products']) && ! empty($_POST['bundled_products']) ) {
            // Get the additional products
            $bundled_products = wc_clean( wp_unslash($_POST['bundled_products']) );
    
            // Get the parent bundle product ID
            $parent_id = isset($_POST['add-to-cart']) && $_POST['add-to-cart'] > 0 ? intval($_POST['add-to-cart']) : null;
            $parent_id = isset($_POST['variation_id']) && $_POST['variation_id'] > 0 ? intval($_POST['variation_id']) : $parent_id ;
    
            // Loop through additional selected product IDs
            foreach ( $bundled_products as $product_id ) {
                $product     = wc_get_product($product_id); // Get the product Object
                $is_variation = $product->is_type('variation'); // Is it a variation
    
                if ( is_object($product) ) {
                    $variation_id = $is_variation ? $product_id : 0;
                    $product_id = $is_variation ? $product->get_parent_id() : $product_id;
                    $attributes = $is_variation ? $product->get_variation_attributes() : [];
    
                    // Add additional product to cart (with the parent bundle product ID as custom cart item data)
                    WC()->cart->add_to_cart($product_id, 1, $variation_id, $attributes, ['parent_id' => $parent_id ]);
                }
            }
        }
    }
    

    Code goes in functions.php file of your child theme (or in a plugin). Tested and work.

    Now the additional products are also added to cart as separated items.


    If you want to use an array of product IDs like in your code, replace:

    if ( $bundled_product_ids = $product->get_meta('_bundled_ids') ) {
    

    with:

    $bundled_product_ids = array(220, 222, 224, 226); // Define your product IDs
    
    if ( $bundled_product_ids ) {
    

    For testing:

    Display cart item data (and custom data) in the footer:

    add_action('wp_footer', 'wp_footer_output_cart_items_data_for_test', 10);
    function wp_footer_output_cart_items_data_for_test() {
        echo '<pre>'. print_r( WC()->session->get('cart'), true ) . '</pre>'; 
    }