Creating a custom Add to Cart form with Drupal Commerce

I'm currently helping some friends rebuild the theological education website, BiblicalTraining.org, in Drupal 7 with Drupal Commerce. I built the Drupal 6 version some years ago to move the website from a bespoke PHP application into Drupal, using modules like Ubercart, Quiz, and Organic Groups to solve most of the requirements. However, the donation code was more or less a straight Drupal port of the PHP script handling donations via PayTrace at the time.

With the rebuild onto Drupal 7, we have the opportunity to unify the donation form with the course payment checkout form to begin using Commerce Reports and multiple payment methods, including the newly released Commerce PayPal 2.0 for Express Checkout payments. However, we still have to deal with actually creating an order to represent the donation payment on the checkout form.

On this site, we don't use product display nodes. I decided instead to directly instantiate the Add to Cart form at a custom URL and avoid the need to create a product display node type just for the one form.

I started by defining a donation product type so the site administrators could create a product to represent the various campaigns donors could give toward. Since I am building the form in a small page callback function, I can easily support as many donation products as get created without introducing the human process of making sure administrators both add the product and update a product display node to reference it.

I created a single menu item at /donate whose page callback is the following:

<?php
/**
 * Page callback: builds a donation Add to Cart form.
 */
function bt_donation_form_page() {
 
// Create the donation line item defaulted to the General fund.
 
$line_item = commerce_product_line_item_new(commerce_product_load(5), 1, 0, array('context' => array('display_path' => 'donate')), 'donation');
 
$wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

 
// Set the line item context to reference all of the donation products.
 
$query = new EntityFieldQuery();
 
$query
   
->entityCondition('entity_type', 'commerce_product')
    ->
entityCondition('bundle', 'donation')
    ->
propertyCondition('status', TRUE);

 
$result = $query->execute();

  if (!empty(
$result['commerce_product'])) {
   
$line_item->data['context']['product_ids'] = array_keys($result['commerce_product']);
  }

 
// Do not allow the Add to Cart form to combine line items.
 
$line_item->data['context']['add_to_cart_combine'] = FALSE;

  return
drupal_get_form('commerce_cart_add_to_cart_form', $line_item);
}
?>

To build an Add to Cart form, you have to pass in a line item object that contains all the information required to build the form. Our page callback starts by building a donation product line item, a custom line item type that allows for custom donation amounts as demonstrated in this tutorial video from Randy Fay. The product the line item references, "General fund", will be the default selection on the Add to Cart form, and the context array I pass to the line item creation function populates the line item's display_path field to link this line item to the donation page.

Next I use a simple EntityFieldQuery to find all the enabled donation products on the site. These product IDs are added to the line item's build context array, which the Add to Cart form builder function uses to know which products the form should represent. In a product display scenario, these product IDs would come from the value of the product reference field. Here I pass them in directly and get a simple Add to Cart form that is now ready to be themed to perfection:


Cameo: Select or Other powers the "Other" option here.

Additional improvements on the site involve changing the "Add to Cart" button to read "Donate Now" and using a custom message upon submission with a redirect to /checkout. In case the donor cancels checkout and ends up at /cart, I do two things to ensure they can't manipulate the quantity of the donation line items: I removed the quantity textfield field from the View, but in case we need to add it back in later for other line item types, I also use a form alter to convert any donation line item quantity textfield to the plain text quantity value:

<?php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function bt_donation_form_views_form_commerce_cart_form_default_alter(&$form, &$form_state) {
 
// Loop over the quantity textfields on the form.
 
foreach (element_children($form['edit_quantity']) as $key) {
   
$line_item_id = $form['edit_quantity'][$key]['#line_item_id'];
   
$line_item = commerce_line_item_load($line_item_id);
   
   
// If it's for a donation line item...
   
if ($line_item->type == 'donation') { 
     
// Turn it into a simple text representation of the quantity.
     
$form['edit_quantity'][$key]['#type'] = 'value';
     
$form['edit_quantity'][$key]['#suffix'] = check_plain($form['edit_quantity'][$key]['#default_value']);
    }
  }
}
?>

This might actually make a handy contrib module...

If the donor leaves a donation line item in the cart and goes back to the donation form, you'll notice toward the end of my page callback that I also indicate in the context array that the form should not attempt to combine like items during the Add to Cart submission process. I actually realized the form builder function was missing some documentation for context keys, so I added those in straightaway.

All told, I spent a couple hours building a custom donation form and workflow that now perfectly integrates with the checkout process used by the rest of the site. This will make it easier to customize and maintain long term, and it allows us to use existing Drupal Commerce payment method modules to manage donations instead of having to write and maintain a custom payment module for the task.

Comments

Ah ha! I'm doing this exactly right now (literally!), plus a recurring donation option via commerce_recurring. There is no code needed for that in addition, other than adding, say, three products to the product reference form for the product display (i opted for this plus panels rather than a page callback): One-Time Donation (simple instance of the donation product type) and then products set up called Monthly Donation and Annual Donation using the recurring product type, with the configured respective intervals, or a recurring product type set up with the 2.x recurring fields.

Is there a way to separate out the one-time payment from the recurring payment so that the one time payment takes place immediately while the recurring payment to only occur according to its specific period?

Sublime.

We did a similar thing with our peer-to-peer rental site project. Custom add to cart forms are where its at if you need to do anything a bit outside the box.

This is great information. I need to create something similar for a site I'm building right now. I am still relatively new to coding and Drupal. Did you place all your above code in a separate module or somewhere else? Thanks