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.

Bypassing Drupal Commerce customer profile duplication

In Drupal Commerce, we deal in entities, fields, and field-based relationships between entities (i.e. references). The extent to which we implemented our data model on these systems from the earliest days of Drupal 7 is what grants Drupal Commerce developers a level of flexibility previously unavailable to eCommerce developers in general. Cool, right? Wink

Unfortunately, the Drupal entity trade can be dangerous when dealing with historical data that isn't supposed to change - when a reference should do more than merely "refer" but also express constraints.

Such is the case with customer profile references on orders.

Once customer information (e.g. a billing or shipping address) is entered for an order, we don't want the order to lose that information at a later date. This means we have to prevent not just the deletion of the referenced customer profiles but also changes to them - the latter because our references are to entitiies, not revisions of entities.

Side note: honestly, I'm fine with that - I can't imagine a reasonable UI or update strategy for entity revision references. It's hard enough to keep straight as is. Tongue

The most common place users encounter customer profile duplication is when they go to update a customer profile through the UI. If any field values are changed, the Order module will prevent the update if it detects the customer profile is referenced by a non-shopping cart order. It empties the profile IDs in a presave hook, forcing the save to create a new customer profile instead of updating the existing one.

This same process affects not just customer profiles being edited through the UI but also customer profiles being updated through code. However, if you combine the fact that customer profiles have revisions with the absence of the duplication related messages you see when performing the operation in the UI, it's easy to see how this functionality might appear to developers as a case of revisions gone awry.

There are a couple of functions developers can refer to to read comments describing the process and see the implementation itself:

  • commerce_order_commerce_customer_profile_presave()
  • commerce_order_commerce_customer_profile_can_delete()

It's worth noting that we've coded this functionality to be paranoid - if there's a slight chance something substantive may have changed in the field data, we force duplication instead of updating the original. Better to duplicate than to lose vital data.

Still, the keen observer will note that we actually do permit customer profiles to be updated through the UI on one condition. If an administrator edits a customer profile through the edit form of the sole order referencing the profile, we permit the update.

You may have a need in custom code to perform an update to a customer profile field that you know does not affect the historical record in a substantive way. Maybe it's simply a matter of changing some internal field that has no bearing on the fulfillment of orders. In such cases, to bypass customer profile duplication, you can imitate the process the order edit form uses to identify an order as safe to delete.

To do this, add a temporary "entity_context" property to the customer profile object. This is the property the Order module looks for in the presave hook to determine if the customer profile is being edited in the context of its sole referencing order. If you properly identify this order in the entity_context, the Order module will permit the update to occur without duplication:

<?php
$profile
= commerce_customer_profile_load(1);
$wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
$wrapper->field_referral_source = 'MySpace';
$profile->entity_context = array(
 
'entity_type' => 'commerce_order',
 
'entity_id' => 1,
);
commerce_customer_profile_save($profile);
?>

Obviously, this is obtuse.

In Commerce 2.x, we'll do well to improve the developer experience here. It gets even worse if you want to make such an update on a site using Commerce Addressbook where you do want to update a customer profile referenced by multiple orders without enacting duplication. In fact, until I wrote this post, there would have been no way to achieve this short of a miraculous hook_query_alter().

This whole post came out of a quick e-mail exchange with Forest Mars, whom I'm looking forward to meeting at next week's Florida DrupalCamp (you going?). Since I've had a chance to think about the issue in a longer form post, I'm going to go ahead and add a query tag so it's at least possible to target the "can delete" query without committing developer sins.

And that is how e-mail and blogging improve open source projects, my friends! Cool

Learn Front End Development at The Iron Yard Academy

A year ago I moved to Greenville, SC to live closer to family. I haven't looked back since, and I've fallen more and more in love with my new hometown every month. I had cursory knowledge of its great sights, food, culture, and climate before we moved, but the big surprise has been the city's vibrant tech community.

A few months after moving, I discovered The Iron Yard, which at the time was running a three month startup accelerator. Peter and the crew were all very welcoming and invited me to hang out with the teams giving and receiving in turn... and of course occasionally talking Drupal. I wrote more about their program and space on my therapeutic moving blog, One Ville to Another.

In recent months, The Iron Yard merged with this awesome co-working space (creatively Wink ) named CoWork. They then scored a 30,000 sq ft office space ("the forge") in the heart of our wonderful downtown to gather not just their programs but also like minded design, development, and creative firms into a single space. They also launched a Coder Dojo to teach kids web development (along with Scratch game programming and Arduino hacking) and are now expanding the school to education for adults, too. Hence the blog post.

Starting next month, The Iron Yard Academy will take a small class of folks through an intense, full-time, three month course in front end development. The two teacher-mentors will be Mason Stewart and Marco Suarez, a killer development / design pair who recently launched the attractive Etsy curation site Haberdash Fox.

Their class will cover the basics (HTML, CSS, JavaScript) and quickly move into pertinent topics for front-end Drupal development, especially with Drupal 8 - Git / the command line, HTML5, jQuery, Backbone.js, responsive design, and more.

In addition to the course, students will get a MacBook Air, lodging in the city (if needed), access to The Iron Yard mentors, and guaranteed job placement (again, if needed) within a year doing front-end development in a startup - hopefully locally. The whole course is only $9,750 and will be repeated a couple more times this year if you can't make it so soon.

The program is definitely open to students who already have experience in web development, even if you're currently employed. If you have an underutilized dev who wants to super charge their front end skills, I'd highly recommend sending them to learn and grow in this great community. I look forward to meeting the new class, and I hope some of the new talent makes its way into the Drupal community. Smile

Toward HyperDrupal

Back in September I had the privilege of attending a web development conference in my new hometown, Greenville, SC. The occasion was REST Fest, which was primarily a barcamp format gathering of REST API developers and thought leaders from around the world.

I got to meet several local web developers along with encouraging and helpful out of towners like Mike Amundsen (author of Building Hypermedia APIs with HTML5 & Node, highly recommended) and Sam Ramji (erstwhile champion of open source at Microsoft, now with Apigee). I also got a second chance to meet David Zuelke, who I somehow missed at DrupalCon Munich where he presented on RESTful Web Services. (In addition to REST, he and I geeked out over the BMW Performance Track, which I enjoyed the day before thanks to demo day at The Iron Yard, our local startup accelerator and host to REST Fest and other tech events.)

The event was quite the eye opener, an opportunity for me to be out of my element with nary a Drupal developer in sight. However, the topics covered and examples shared were incredibly practical for me, as we were in the middle of researching and implementing RESTful Drupal Commerce at Commerce Guys for a client project and for a Commerce Kickstart mobile app developed with the help of Sumit Kataria.

Fast forward now three months and I'm finally circling back around to pump up the Commerce Services module to turn any Drupal Commerce site into a REST API server. My focus thus far has been on improving GET requests to support a variety of query parameters offering:

  • Response format specification (i.e. a subset of an entity's properties / fields when you don't need the entire entity)
  • Filtering by property names, field names, and multi-column field column names
  • Sorting by those same parameters
  • Paging through the result set via limit and offset parameters
  • The ability to include referenced entities in the response
  • The ability to flatten field value arrays to the current language and from arrays to scalars for single value, mono-column fields

As a best practices guide, I've been using an incredibly helpful eBook from Apigee called Web API Design: Creating Interfaces that Developers Love. The idea here is to follow standard patterns for all of our entity resources that allow API clients to easily browse the API.

Additionally, my goal is for Drupal Commerce API servers to actually be RESTful, not simply RESTish. If you aren't familiar, a great articule to bookmark and read when you need it is A Brief Introduction to REST by Stefan Tilkov. As we build out the Commerce Services module and deploy projects from client sites to mobile apps, we'll also publish developer documentation that defines and demonstrates best practices use of the system.

My end goal here is to keep pushing forward to a holy grail of sorts - HyperDrupal. By simply creating powerful resources that respect HTTP methods (and other headers) as defined, we're two thirds of the way there. The final step (as identified by the Richardson Maturity Model) is to format our responses using a "hypermedia type" - a media type that includes the hypermedia controls we're used to in HTML like links (a), forms (forms that POST), query templates (forms that GET), and the like. See Mike Amundsen's book (referenced above) for more detail.

The media type I'll be focusing on first is Mike's Collection+JSON media type. Even as I do my testing with Commerce Services, I'm using the beginnings of what I hope will become the Collection+JSON response formatter for Services based REST servers. The "big idea" here is that an API client like a mobile app could browse your API like a user would browse your website.

For example, the app would start at the "front page" of your API where it might find the link to your product list. When the customer chooses which product to view, in addition to the product information, the GET response includes a form with the necessary parameters to submit a POST request to add the product to the cart. If built to consume this information properly, the client never actually need to know how your URLs are constructed, whether or not you've recently changed API responses, or when you've added new features to the site. That information would all be communicated through API request responses.

This process has been awkwardly acronymized as "HATEOS" (pronounced had-ee-us) - Hypermedia as the Engine of Application State - and is further described in Haters Gonna HATEOS by Steve Klabnik, proprietor of the living eBook Designing Hypermedia APIs. It may sound a bit "pie in the sky", but the advantages of Drupal / Drupal Commerce offering this level of API functionality out of the box are too good not to pursue.

The good news is that Drupal 8 is well on its way toward beefing up our REST cred. Larry Garfield, Klaus Purer, Lin Clark and others have been hard at work laying the groundwork for solid REST in core through the "Web Services and Context in Core" D8 initiative. I don't believe hypermedia is on the menu for D8 release, but they were just granted a reprieve in the form of the D8 feature completion phase to ensure solid core support for levels 1 and 2 in the Richardson Maturity Model of REST APIs.

Being able to focus specifically on Drupal Commerce helps me research and iterate quickly to implement and use hypermedia with Commerce Services, but it does keep me a bit downstream of all that's going on in core (and for D7 in modules like Services, RESTws, and others). What I can say from here is that the future looks bright, and more members of the community are engaging around and communicating about proper REST API design. Together, I'm confident we'll see a successful push toward HyperDrupal in the very near future. Cool

Pages