Aelia - News badge

WooCommerce Tips & Tricks – Only allow specific product combinations in cart

This post was written in December 2015. Based on our tests, the code works with with WooCommerce 2.5 and 2.6. You might have to adjust it to make it work on newer WooCommerce versions, or adapt it to your specific configuration.

A member of the Advanced WooCommerce group on Facebook presented an interesting challenge. She needed to allow customers to purchase any products freely, except in one case. She had a specific product (let’s calls it Product X) that had to be purchased “alone”, without any other product being present in the cart. In short:

  • If Product X is in the cart, that must be the only product in the cart.
  • If Product X is not in the cart, any other product can be added to it and purchased at the same time.

Our friend Rodolfo, from BusinessBloomer, posted a solution that he adapted from his solution to allow only one product to the cart. It works, but in our opinion, that approach presented a few limitations:

  1. It works by emptying the cart when Product X is added after the other products. If a customer adds Product X to the cart, then he can add other products, and they will stay there.
  2. It doesn’t allow to have combinations of products (e.g. Product X and Product Y allowed together).
  3. It doesn’t make clear to the customer that other products cannot be purchased together with Product X (all products retain their “Add to cart” button, even if they should not).
  4. It empties the cart explicitly. We try to avoid this type of calls whenever possible, and rely on WooCommerce’s internal logic to decide what items should be removed, and when.

Our approach

Taking advantage of the experience gained with the development of our Prices by Country plugin, we prepared a different solution, which, in our opinion, is more flexible and user friendly. It brings the following advantages:

  • It covers the requirement described above, where a specific product (e.g. Product X) must be the only one in the cart.
  • It also allows to have more than one product allowed in the cart (e.g. Product X and Product Y), while excluding all others.
  • It clearly informs the customers that some products can’t be purchased anymore.

You can find it below, described step by step. The code can be added to the theme’s functions.php, or packaged in a plugin, if needed. It has been tested with WooCommerce up to version 2.5.

Step 1 – Keep track of what’s in the cart

The first thing to do is to determine what is in the cart. The content of the cart will dictate what else can be added to it. We do this operation only once per page load, for better performance.

/**
 * Retrieves the cart contents. We can't just call WC_Cart::get_cart(), because
 * such method runs multiple actions and filters, which we don't want to trigger
 * at this stage.
 *
 * @author Aelia <support@aelia.co>
 */
function aelia_get_cart_contents() {
  $cart_contents = array();
  /**
   * Load the cart object. This defaults to the persistant cart if null.
   */
  $cart = WC()->session->get( 'cart', null );

  if ( is_null( $cart ) && ( $saved_cart = get_user_meta( get_current_user_id(), '_woocommerce_persistent_cart', true ) ) ) {
    $cart = $saved_cart['cart'];
  } elseif ( is_null( $cart ) ) {
    $cart = array();
  }

  if ( is_array( $cart ) ) {
    foreach ( $cart as $key => $values ) {
      $_product = wc_get_product( $values['variation_id'] ? $values['variation_id'] : $values['product_id'] );

      if ( ! empty( $_product ) && $_product->exists() && $values['quantity'] > 0 ) {
        if ( $_product->is_purchasable() ) {
          // Put session data into array. Run through filter so other plugins can load their own session data
          $session_data = array_merge( $values, array( 'data' => $_product ) );
          $cart_contents[ $key ] = apply_filters( 'woocommerce_get_cart_item_from_session', $session_data, $values, $key );
        }
      }
    }
  }
  return $cart_contents;
}

// Step 1 - Keep track of cart contents
add_action('wp_loaded', function() {
  // If there is no session, then we don't have a cart and we should not take
  // any action
  if(!is_object(WC()->session)) {
    return;
  }

  // This variable must be global, we will need it later. If this code were
  // packaged as a plugin, a property could be used instead
  global $allowed_cart_items;
  // We decided that products with ID 737 and 832 can go together. If any of them
  // is in the cart, all other products cannot be added to it
  global $restricted_cart_items;
  $restricted_cart_items = array(
    737,
    832,
  );

  // "Snoop" into the cart contents, without actually loading the whole cart
  foreach(aelia_get_cart_contents() as $item) {
    if(in_array($item['data']->id, $restricted_cart_items)) {
      $allowed_cart_items[] = $item['data']->id;

      // If you need to allow MULTIPLE restricted items in the cart, comment
      // the line below
      break;
    }
  }
});

Step 2 – Prevent disallowed product combinations

Now that we know what’s in the cart, we can prevent some products from being added to it if any of the “restricted” products are present. Emptying the cart would not work, as we would risk to throw away one of the allowed products. Instead, we simply make the disallowed products unavailable. This will have several effects:

  • If any of the disallowed products is already in the cart, WooCommerce will remove it.
  • The Add to Cart button will be replaced by a Read More button on the disallowed products. Customers won’t be able to add the products back, and will instead get a note explaining that they cannot be purchased.
// Step 2 - Make disallowed products "not purchasable"
add_filter('woocommerce_is_purchasable', function($is_purchasable, $product) {
  global $restricted_cart_items;
  global $allowed_cart_items;

  // If any of the restricted products is in the cart, any other must be made
  // "not purchasable"
  if(!empty($allowed_cart_items)) {
    // To allow MULTIPLE products from the restricted ones, use the line below
    //$is_purchasable = in_array($product->id, $allowed_cart_items) || in_array($product->id, $restricted_cart_items);

    // To allow a SINGLE  products from the restricted ones, use the line below
    $is_purchasable = in_array($product->id, $allowed_cart_items);
  }
  return $is_purchasable;
}, 10, 2);

At this stage, we have the code that fulfils the original requirements. However, we need one extra step to make it more elegant.

Step 3 – Explain customers why some products cannot be purchased anymore

As we have seen, the code in step 2 prevents some products from being added to the cart if Product X and/or Product Y are already present, but it doesn’t explain customers why. We just need to show them a message with some information about the restrictions, to make things clearer.

// Step 3 - Explain customers why they can't add some products to the cart
add_filter('woocommerce_get_price_html', function($price_html, $product) {
  if(!$product->is_purchasable() && is_product()) {
    $price_html .= '<p>' . __('This product cannot be purchased together with "Product X" or "Product Y". If you wish to buy this product, please remove the other products from the cart.', 'woocommerce') . '</p>';
  }
  return $price_html;
}, 10, 2);

Step 4 – Combining the code

For the snippets above to work together, we must combine them in the correct order. More specifically, the code from step 2 should go inside the code from step 1. Here’s the complete code, ready to be pasted in the functions.php file: http://pastebin.com/BRU1BP2E.

Step 5, 6, 7, etc – Improvements

The above solution is fully functional, but it would be possible to make it more elegant and flexible. Further improvements to the code could include the following:

  • Packaging the code as a plugin. This will help avoiding global variables and could make the code tidier and easier to read.
  • Adding support for groups of restricted products (e.g. Product X and Y or Product A and B, etc).
  • Adding a dynamically generated message, showing exactly which restricted products are in the cart, instead of relying on static text.
  • Adding formatting to the message displayed to the customers.

Should you need assistance adapting the solution to your needs, or implementing any of the above optimisations, please feel free to contact us. We will review your specifications and provide you with a quote for your customisation.

The Aelia Team

43 replies
  1. WillyVB91 says:

    Hello,

    Sorry for the disturb I have some questions to do, is possible speak with you by email or on a social page/forum?

    Reply
    • diego says:

      Hi Willy, your comment was filtered by mistake by our antispam system. I see that you already found our support portal. 🙂

      Reply
  2. vincenzo says:

    Hi thanks for your article it is really helpful. Is it possible to use products categories instead of single products? I’ve created two categories one with products I can buy with real money and another one with products I can by with points. Woo Commerce doesn’t permit any payment if these products are together in the cart because the plugin can’t split the cart with two different payment gateway. Unless you have any other solution i think i need to use your snippet.
    Many thanks for your help

    Reply
      • vincenzo says:

        Hi many thanks for your help.
        If i can ask, how do you send email to inform user their comments is awaiting for approval after they post a comment? It’s really amazing. Many thanks

        Reply
        • diego says:

          I think it’s done by the Akismet plugin. We haven’t configured anything specific to send that notification, it’s sent automatically. 🙂

          Reply
          • vincenzo says:

            Hi Diego, thanks for your answer.
            Is there any way to avoid a user to purchase a product if he has already mouth another product? For example i’ve got two products A and B. If user has bought product A he can’t buy product B
            Many thanks

          • diego says:

            That should also be possible, you will simply have to check customer’s previous purchases. You can check if the customer bought a product by calling the aptly named function wc_customer_bought_product(). Simply check, for Product A, if customer already bought Product B, and vice-versa, and you will be able to make the products available, or unavailable, dynamically.

          • vincenzo says:

            Hi Diego,
            Many thanks for your answer. Unfortunately i’m not very good with coding in php. I thought i had to use the function you suggested but i can’t find anything on the web. Do you think you can help me?
            Many many thanks

          • diego says:

            If you need a full, ready made customisation, we can definitely write one for you. The only “wrinkle” is that it might take some work and we might not be able to do it free of charge. If you are interested in discussing it, please feel free to get in touch. We will be closed tomorrow (it’s St Patrick’s, here in Ireland), but we will get back to you as soon as possible.

    • John Hoff says:

      Hi vincenzo 🙂 I’m writing to find out if you were able to make the adjustment from product ID to product category. I would greatly appreciate any insight you may be able to provide on how to make this adjustment.

      Thank you!

      Reply
  3. Zakaria Shahed says:

    Hello sir
    I Have 4 product suppose A,B,C,D,E,F,G,H and here i want If i add A in my cart then It will restrict for B,C,D only and shows massage.That User Need to buy separately but they can buy this A and F together

    Reply
    • diego says:

      The article describes exactly how to do that. You just have to look at the $restricted_cart_items element in the code. In that array you can enter the products that can go together in the cart (in your case, A and F).

      Reply
        • diego says:

          The code is already there, ready to be used. You just need to set the correct product IDs and it will work, out of the box. 🙂
          If you need assistance implementing a specific customisation, we offer consultancy services as well. If you wish to avail of our services, our standard rate is 80 EUR/hour, plus applicable VAT.

          Reply
  4. Roz says:

    This is exactly what I am looking for, to not allow a specific product to be in cart with any other products from our store, yet your code gives me multiple red flag syntax errors when I paste it into my functions.php (I do this in dreamweaver)

    Reply
    • diego says:

      Hi Roz,
      The code works fine on our servers. Make sure that you copy it from the pastebin link, as the article itself may contain formatting elements that are not valid PHP code. Also, the code is tested on PHP 5.3 or later. If your editor uses an older version for parsing, then it will show you some errors, but the code may run fine on your site.

      Reply
    • diego says:

      I’m afraid that I can’t say what the exact issue could be. The code from http://pastebin.com/raw/BRU1BP2E is correct, the issue you see seems to be caused by something else. The message indicates line 247, which is above the code you show in your screenshot. Most likely there is an error somewhere else, and this causes a “cascade” of syntax errors down the line.

      Reply
  5. Roz says:

    Ive even pasted it as the top, and as the only code in the file and it still gives me the same syntax errors. I tried pasting it in an online sytax checker and it says its ok so its definitely dreamweavers problem. I’ll give the customization a try. thanks!

    Reply
  6. Roz says:

    I found the perfect addition to your script. The latest version of WC doesn’t seem to show your error message but if users install this plugin, they can make changes to any error message they want, which will tie in your awesome script and allow for explanations of product removals, etc.

    https://wordpress.org/plugins/say-what/faq/

    Thanks again for your quick replies!

    Reply
    • diego says:

      Hi Roz,
      Thanks for the update. That’s right, we haven’t tested the snippet with WooCommerce 2.6 (if you look in the code, it specifies that it was tested up to WooCommerce 2.5). I would be surprised if they broke the way that error messages are displayed, but it may be worth having a look at that later. Thanks again for your feedback.

      Reply
  7. Iara says:

    I love it, thank you!
    But it does not seem to work in the woocommerce new version (Fatal error). T________________T

    Reply
    • diego says:

      Hi Iara,
      Your comment was filtered by our system as spam by mistake, sorry about that.

      The code you found on our article works fine in WooCommerce 2.6, it doesn’t cause any fatal error. Perhaps you made some mistake in implementing it. One of the most common issues is pasting it after a closing PHP tag (i.e. ?>), or using only part of it. Another relatively common issue is pasting multiple copies of the code (we had a few users who did it, in the past, thus causing errors due to duplicate functions).
      If you use the complete code from Pastebin, it should work just fine.

      Reply
  8. Daniel says:

    Hi,

    I put the code inside wp-content/plugins/myfolder/myfile.php.

    When i activate it, its output all the code in the top of the wp-admin.

    Can you help please,
    Daniel

    Reply
    • diego says:

      Most probably you just forgot the opening PHP tag (i.e. <?php). Also, keep in mind that, if you want to put the code in a plugin, you will also have to add a plugin header (see Writing a Plugin).

      Reply
  9. Kedar says:

    Hi there! This snippet works great, but if the user adds a non-restricted product FIRST, then they’re still able to add restricted items. Would there be a way to prevent that?

    Reply
    • diego says:

      Hi Kedar,
      You’re right: the checks only verify if a restricted product is in the cart. If none of the restricted products are in the cart, the existing logic assumes that any product can be added to it (restricted or not). You should be able to extend the check by modifying the woocommerce_is_purchasable function in the snippet. The check should be something like the following (off the top of my head, I haven’t implemented it): if the cart is not empty and doesn’t contain restricted products, and the product being checked is restricted, then that product should not be purchasable.

      Reply
  10. Andy Hinkle says:

    Hello, this does not work with the latest version of WordPress. I was able to add different categories after I added the Pastebin to functions.php

    Reply
    • diego says:

      The code doesn’t check for product categories, but for single product IDs. If you need to allow specific product categories, then you will have to adapt the code for that purpose. You can find a code snippet to retrieve the categories for a product here: WooCommerce Tips & Tricks – Get all the categories to which a product belongs.

      Please also keep in mind that are several elements that come into play when a product is added to the cart (other plugins, other custom code, etc), and some of them might interfere with the code we wrote. If you need a custom solution tailored to your needs, please feel free to contact us to request a consultation.

      Reply
  11. Reese says:

    Hello, and good afternoon 🙂

    I am using WC v. 3.0.4, and I would like to use this plugin to only allow certain products B & C, in the cart IF the customer already has added product A. I have already set B & C as up-sells for A, and B&C area also hidden from the catalog view, but I do not want the customer to be able to add them unless A is already in cart, and if possible, that B&C are removed from cart if A is removed.

    Thank you 🙂
    — Reese

    Reply
    • diego says:

      This custom code is designed to prevent customers from adding a product to the cart if specific ones are already in the cart. What you are trying to do is the opposite, i.e. preventing the purchase if some specific product() is not in the cart. For that purpose, you will need a different customisation. If you wish, you can contact our support and consultancy service, and they can prepare an estimate to implement this customisation.

      Reply
  12. Dan says:

    Thanks so much for this snippet. I’m struggling because I keep getting “Call to undefined function WC()” on line 50 and all my troubleshooting doesn’t seem to help. Any thoughts? Thanks!

    Reply
    • diego says:

      The `WC()` function is part of WooCommerce core. If you use the code from our Pastebin, then it will run when the wp_loaded event is triggered. That event runs after WooCommerce has been fully initialised, therefore function `WC()` is guaranteed to be exist. If that is not the case, I would guess that either WooCommerce is not configured, or not loaded correctly.

      Also, we are not aware of any conflicts with the Stripe payment gateways. The custom code runs before any payment gateway code is used, and should not cause any conflict. However, as the article explains, we wrote the code in December 2015. It might need some adjustments to work with WooCommerce 3.0.x (we haven’t prepared a revised version for it, yet).

      Reply
  13. Ricardo says:

    Hello Diego

    Thank you for your code, it’s really helpfull and you are the only to talk about that.

    However in my case i can’t figure how to modify your function for my needs.

    I have bunch of “physical” products and 2 subscription products call “premium” and “premium+”
    I add “add to cart button” for each of them below the cart directly (it’s options like amazon premium service).

    “Premium” give a certain amount of services
    “Premium +” offer all services provide by “premium” with extra.

    My scenario: Customers can add as “physical” products as they want in their cart but can add only one of these 2 premium services (premium OR premium plus).

    If they already add “premium” to the cart and then want to add “premium plus” on the same cart, it must display a message telling “sorry but you must remove the other premium service before”

    Thank you for your help

    Reply
    • diego says:

      The code is designed to do what you describe. If you add the IDs of the two “premium” and “premium+” products to variable $restricted_cart_items (see Step 1), only one of the two can be added to the cart at any given time. Then you can customise the message in Step 3 to invite the user to remove the premium service from the cart before adding the other one.

      Please keep in mind that the code was designed for WooCommerce 2.5 and 2.6, as indicated at the top of the article. We haven’t tested it with WooCommerce 3.x, it might require some adjustments for those versions. If you would like to have the custom code tailored to your specific needs, please feel free to get in touch. We can review your specifications and schedule a consultation to package the code into a standalone plugin that you can install to your site.

      Reply
  14. Melissa Boverhof says:

    So the array function is not working for me. (See Below) Once one of the restricted products is added to the cart, it will not allow any more products to be added. Any ideas why this is happening?

    // This variable must be global, we will need it later. If this code were
    // packaged as a plugin, a property could be used instead
    global $allowed_cart_items;
    // We decided that products with ID 737 and 832 can go together. If any of them
    // is in the cart, all other products cannot be added to it
    global $restricted_cart_items;
    $restricted_cart_items = array(
    1000778,
    1000779,
    );

    Reply
    • diego says:

      That’s correct. The code is designed to prevent customers from adding any other product to the cart, if any of the products in the $restricted_cart_items list is present. With your code, it means that you can add product 1000778 and 1000779 to the cart at the same time. The code will prevent you from adding any other product, as long as either of the two is in the cart.

      Reply
  15. Marcvill says:

    Hi, would just like to ask if this code/solution still works without issues using the latest WordPress and WooCommerce versions? Thanks 🙂

    Reply
    • diego says:

      The WordPress version should not make any difference. We haven’t tested the code with WooCommerce 3.x, but it could still work. We would just recommend to replace calls like `$product->id` with `$product->get_id()`, as accessing properties directly is a deprecated approach in WooCommerce 3.0 and newer.

      Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *