Creating Drupal Commerce add to cart links

By shane
Wed, 2014-06-25 07:29
comments

Share with Others

I recently ran across a problem of needing multiple Drupal Commerce add to cart buttons on a single page. Instead of trying to add a second add to cart form, I decided it would be easiest to create a custom link that could then be used to add products to the cart. This is useful if you need to create multiple add to cart links on the same page, or if you just need to create custom add to cart links to place in other areas of your Drupal Commerce website.

Updated Solution

So after trying all of these solutions, I found out that none of the solutions worked as well as I would like. Because of this, I found a secure, reliable (so far) solution. I will leave the original post below for reference, but here is how I would suggest adding an add to cart button. The solution uses tokens to allow you to add a token to the body field of a node. This token will be replaced with an add to cart form when the user views the page. This will allow you to have multiple add to cart forms for the same product, on the same page. This add to cart form is often just an add to cart button (unless you use a quantity field, or have multiple product references for a single product display node). This means you are essentially able to add as many add to cart buttons as you want on a single page. This is useful for longer sales/landing pages with a lot of content. You may want an add to cart button in the middle, and one again at the bottom of the page.

I created a Sandbox project on Drupal.org with the module I outline below. Go ahead and give it a try and let me know what you think If it seems to work well for other people I will get it pushed into an official Drupal.org project.

https://www.drupal.org/sandbox/smthomas/2320659

Building the Add to Cart Form Token functionality

The first step was to download, install, and configure the Token Filter module. This will allow you to use tokens on your body field.

Next you will need to create a custom module with the following code in the .module file (replace MYMODULE with the name of your module):

/**
 * Implements hook_token_info().
 */
function MYMODULE_token_info() {
  $type = array(
    'name' => t('Add to Cart form'),
    'description' => t('Tokens to create an add to cart form.'),
  );
 
  return array(
    'types' => array('addtocartform' => $type),
  );
 
  return $info;
}
 
/**
 * Implements hook_tokens().
 */
function MYMODULE_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'addtocartform') {
    foreach ($tokens as $name => $original) {
      list($product_id, $quantity) = explode(':', $name);
      $product = commerce_product_load($product_id);
      $line_item = commerce_product_line_item_new($product, $quantity);
      $line_item->data['context']['product_ids'] = array($product_id);
      $form = drupal_get_form('commerce_cart_add_to_cart_form', $line_item);
      $replacements[$original] = drupal_render($form);
    }
  }
  return $replacements;
}

So what does this do? Well you can add the following token to your body field now and the token will be replaced with an add to cart form:

[addtocartform:23:1]

This will create an add to cart form that adds a Quantity of 1 of Product ID 23 to your cart when clicked. If you want to add 2 of product id 55, your token would be:

[addtocartform:55:2]

Note: I had to create a new Full HTML text format that didn't have the automatic line breaks in order to get the Add to cart button to display correctly. You will need to make sure you allow full HTML in any body field that uses the addtocartform token.

------------------------------------------------------
---------------------------------------------------------
-----------------original post-------------------------
---------------------------------------------------------------
Here is the original post with some other alternatives/ideas on how to accomplish this same thing:

I broke this down into 3 different sections. The first goes over the original solution, the second covers a more complicated but more secure option, and the last lists an alternative (no code) solution.

Creating Multiple Add to Cart Links

WARNING: Be aware that this approach introduces possible CSRF vulnerabilities as the link can be used to add any product to your cart. I kept this posted as there are some who may be OK with this risk. You have been warned!

To do this I used a custom Drupal 7 module and started by implementing hook_menu().

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
 
}

The next step was to implement the MYMODULE_add_to_cart function. After doing a little research, I found the commerce_cart_product_add_by_id function.

This function needs to accept the product_id that is being passed from the hook_menu function, will call the commerce_cart_product_add_by_id function, and will then redirect the user to the cart page.

/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Now using a link such as:

<a href="/commerce/add-to-cart/1">Add to Cart</a>

I could add Product ID 1 to my cart. This would now work for any product id (just replace the number 1 with the correct product id number). Any link to commerce/add-to-cart/[product-id] will now work across my entire Drupal site.

This worked great for my needs, however it got me thinking of other possible use cases. What if we didn't know or have access to the product id (maybe you have a view that lists product nodes and you don't want to add the extra relationships). You could make this function work by node id instead of product id by changing the code to:

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/Creating Drupal Commerce add to cart links'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
}
 
/**
 * Programmatically adds an item to a cart by node id.
 */
function MYMODULE_add_to_cart($node) {
  // Get the product reference from the node.
  $product = field_get_items('node', $node, 'field_product_reference');
 
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product[0]['product_id']);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Keep in mind the first way is the more preferable option. Also keep in mind this will only work if the node to product reference is one to one. If you have multiple products being referenced from the product display node, this will only add the first product referenced.

You may also want to be able to specify the quantity from the link, this is also very easy to add. The commerce_cart_product_add_by_id function accepts a second parameter for the quantity. Making the change to the code is pretty simple:

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access checkout'),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2, 3),
  );
  return $items;
}
 
/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id, $quantity = 1) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id, $quantity);
 
  // Go to the cart page.
  drupal_goto('cart');
}

Notice how the only changes needed were to add an additional page argument to the menu item, and then add that to the commerce_cart_product_add_by_id function call. I could have added an additional wildcard character (%) to the end of the menu item making it commerce/add-to-cart/%/% but that would have made the quantity required. This way the quantity value is optional and will default to 1 if left out.

You could now use all of the following links.

To add a quantity of 5 of Product ID 1:

<a href="/commerce/add-to-cart/1/5">Add to Cart</a>

To add a quantity of 1 of Product ID 23 (it will default the quantity to 1 if you leave it out):

<a href="/commerce/add-to-cart/23">Add to Cart</a>

Note: I am using the first example (using the product id not the node id option) since that is the most preferable implementation.

More secure Add to Cart Links

This approach is similar to the approach listed above, but slightly more complicated. In order to secure from a CSRF vulnerability, there needs to be a dynamically generated token on the end of the URL. This however makes adding a link in the content of a Node difficult (I needed the link to be in the body field on a content type). I came up with a token based solution to get this to work.

The first step was to download, install, and configure the Token Filter module. This will allow you to use tokens on your body field.

The next step is to create a custom module, or add code to an existing custom module. This code is similar to the code above, but has a few important differences.

The first big difference is that the menu item has an access callback. This access callback verifies that the link that was clicked has a correct token value added to the end of the link. The second big change is the addition of an addtocart token type (read below the module code for examples of what this does):

/**
 * Implements hook_menu().
 */
function MYMODULE_menu() {
  $items = array();
  $items['commerce/add-to-cart/%'] = array(
    'title' => 'Add item to cart',
    'type' => MENU_CALLBACK,
    'access callback' => 'MYMODULE_add_to_cart_access',
    'access arguments' => array(2),
    'page callback' => 'MYMODULE_add_to_cart',
    'page arguments' => array(2),
  );
 
  return $items;
}
 
/**
 * Access callback to verify user has correct permissions and token.
 */
function MYMODULE_add_to_cart_access($product_id, $quantity = 1) {
  $token = empty($_GET['token'])?"":$_GET['token'];
  $valid = drupal_valid_token($token, 'addtocart' . $product_id . $quantity);
 
  // If the token is valid and the user can access checkout, the user has access.
  if ($valid && user_access('access checkout')) {
    return TRUE;
  }
 
  return FALSE;
}
 
/**
 * Programmatically adds an item to a cart by product id.
 */
function MYMODULE_add_to_cart($product_id, $quantity = 1) {
  // Add the product to the current users cart.
  commerce_cart_product_add_by_id($product_id, $quantity);
 
  // Go to the cart page.
  drupal_goto('cart');
}
 
/**
 * Implements hook_token_info().
 */
function MYMODULE_token_info() {
  $type = array(
    'name' => t('Add To Cart'),
    'description' => t('Tokens to add items to a users cart.'),
  );
 
  return array(
    'types' => array('addtocart' => $type),
  );
 
  return $info;
}
 
/**
 * Implements hook_tokens().
 */
function MYMODULE_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $replacements = array();
  if ($type == 'addtocart') {
    foreach ($tokens as $name => $original) {
      list($product_id, $quantity) = explode(':', $name);
      $token = drupal_get_token('addtocart' . $product_id . $quantity);
      $replacements[$original] = l('Add to Cart', 'commerce/add-to-cart/' . $product_id . '/' . $quantity, array('query' => array('token' => $token)));
    }
  }
  return $replacements;
}

So what does this do? Well you can add the following token to your body field now and the token will be replaced with a safer more secure add to cart link:

[addtocart:23:1]

This will create a link that adds a Quantity of 1 of Product ID 23 to your cart when clicked. If you want to add 2 of product id 55, your token would be:

[addtocart:55:2]

The link will be created for you when your page is rendered.

Note: This has been mostly untested besides a few basic tests. There could be issues with this that I haven't run into yet. If you do test it and have feedback, please let me know.

Using Rules Link as an alternative

It was also mentioned in the comments that the Rules Link module could be used to accomplish almost the same thing. I have not used this module so I don't know exactly how it works, but it appears that it would work. You could build the functionality as a Rule and then execute that rule by using a Rules Link. I will try to take a look at this module further and report back how this would need to be set up.

How else could this be used? Do you have any questions on ways to modify this to work for you? Let me know in the comments.