Drupal Commerce Blog

What's happening in the world of Drupal Commerce.

Implementing a Commerce License plugin

One of the greatest additions to the Drupal Commerce contributed module space this year is the Commerce License and Commerce License Billing suite. Together, these modules provide a way to sell access to electronic resources, and track recurring orders, measure usage of resources, and bill customers accordingly. This is a use case that isn't well-addressed by many other popular e-commerce platforms, which are typically focused on shipping physical products rather than managing access to electronic resources like files, software, user accounts, or online content. Commerce License provides a way to tackle essentially any of these use cases using a combination of the Drupal Entity API module and CTools plugins delivered through a module called Entity Bundle Plugin. Bojan Živanovic, the module suite's author, has written about this methodology extensively here: http://bojanz.wordpress.com/2013/07/19/entity-bundle-plugin/

The upshot of this plugin-based functionality is that granting access to a totally new type of electronic resource is just a matter of creating an appropriate Commerce License class and implementing a small administrative UI. Based on some feedback in the #drupal-commerce IRC channel a few weeks ago, we decided to take a look at demoing this process by creating a Commerce License plugin for the popular Organic Groups module -- allowing you to sell group membership to any group entity on your Drupal site. We'll walk through each of the required pieces of the plugin and the Commerce License API in turn! First, we'll look at the files our planned module will need:

.
|-- commerce_license_og.info
|-- commerce_license_og.install
|-- commerce_license_og.module
|-- includes
|   `-- commerce_license_og.admin.inc
`-- plugins
    `-- license_type
        |-- CommerceLicenseOg.class.php
        `-- og.inc

The elements here consist of a module file with most of our hooks and some logic to create fields on our license products, an admin.inc file that controls the administrative UI for enabling our new license type, and the license class itself. The og.inc file is the CTools plugin file which tells Entity Bundle Plugin that our class (and thus, Commerce License bundle) exists.

Enabling OG Licenses for Products

The first problem to tackle is the administrative UI element. For each license type, the providing module can (and probably should!) implement an administrative UI to choose which license product types can use our plugin. This allows us to set up one license product type which can license roles, another that can only license organic groups, and a third that might license files. These are typically found at the admin/commerce/config/license/[license_type] path. Here is our hook_menu:

<?php
function commerce_license_og_menu() {
 
$items['admin/commerce/config/license/og'] = array(
   
'title' => 'Organic Groups',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('commerce_license_og_settings_form'),
   
'access arguments' => array('administer licenses'),
   
'type' => MENU_LOCAL_TASK,
   
'file' => 'includes/commerce_license_og.admin.inc',
  );

  return
$items;
}
?>

With that taken care of, we can move on to creating our settings form. We will use the system_settings_form function to save our product license type settings as a single nested variable, so all we need to do is set up our form with settings for each licenseable product type. We can retrieve all licenseable product types using the commerce_license_product_types() function. We use this to set up a $product_types array that we would like to make available in our settings form, like so:

<?php
 
// Create a list of licensable product types and their labels.
 
$license_product_types = commerce_license_product_types();
 
$product_types = array();
  foreach (
commerce_product_type_options_list() as $type => $label) {
    if (
in_array($type, $license_product_types)) {
     
$product_types[$type] = $label;
    }
  }
?>

Once we have done that, we set up our form container and get our initial settings. The commerce_license_og_product_types() function is a helper function that returns the current settings from our variable. I abbreviated this section of the form to save myself some typing:

<?php
 
// Our current settings, for use in #default_values in the form.
 
$settings = commerce_license_og_product_types();

 
$form['commerce_license_og_product_types'] = array(
   
'#title' => t('Product types'),
   
'#type' => 'container',
   
'#tree' => TRUE,
   
'#description' => t('Each product type can be enabled and configured for licensing.'),
  );
 
$container = &$form['commerce_license_og_product_types'];
?>

Settings for Product Types

Now that we have done this, we can loop over each available product type and create a settings form. We needed two elements: First, a checkbox to enable this product type for Organic Groups licenses, and second a selection field to determine which "group type" this product type's licenses can grant access to. While Organic Groups (version 2) can be of any entity type, a given OG reference field can only point at a single entity type at a time. This means we need to pick the entity type at this point, because these settings are going to determine how our product's OG reference field is created. We set up the form like this:

<?php
 
foreach ($product_types as $name => $label) {
   
$container[$name] = array(
     
'#type' => 'fieldset',
     
'#title' => $label,
     
'#collapsible' => FALSE,
    );
   
$container[$name]['enabled'] = array(
     
'#type' => 'checkbox',
     
'#default_value' => !empty($settings[$name]['enabled']),
     
'#title' => t('Licensing Enabled'),
    );
   
$container[$name]['entity_type'] = array(
     
'#type' => 'select',
     
'#title' => t('Group type'),
     
'#description' => t('Choose a type of group to license.'),
     
'#default_value' => (isset($settings[$name]['entity_type']) ? $settings[$name]['entity_type'] : NULL),
     
'#options' => commerce_license_og_entity_types(),
     
'#states' => array(
       
'visible' => array(
         
':input[name="commerce_license_og_product_types\[' . $name . '\]\[enabled\]"]' => array('checked' => TRUE),
        ),
       
'required' => array(
         
':input[name="commerce_license_og_product_types\[' . $name . '\]\[enabled\]"]' => array('checked' => TRUE),
        ),       
      ),
    );
  }
 
 
$form = system_settings_form($form);
 
$form['#submit'][] = 'commerce_license_og_settings_form_submit';
?>

As you can see, I did just a little bit of magic with the States API to hide the "entity type" selection widget until a given product type was actually enabled. Playing with this on a Commerce Kickstart install ends up looking like this:

(Note: Obviously trying to license drinks or hats doesn't make sense in the real world. This is just an example! Don't try to license a hat at home.)

Creating OG Selection Fields on Products

Finally, we add an extra submit function. This submit function is used to run our configuration function, like so:

<?php
function commerce_license_og_settings_form_submit($form, &$form_state) {
 
// This will create the cl_og_[entity_type] field on any newly selected product
  // types, and remove it from any newly deselected product types.
 
commerce_license_og_flush_caches();
}
?>

This submit handler manually invokes our "on cache clear" hook. The cache clear hook in turn fires off our commerce_license_og_configure_product_types function. This function will look at our settings variable (which is saved to the database by the magic of the system_settings_form function) and create fields for each OG licenseable product type based on our settings. We want to end up with our product types looking something like this:

If we look to the commerce_license_role module for an example, we see that it uses a similar function to instantiate its role selection field on each licensable product type. This is what allows the store administrator to set the role granted by each license product individually. We'll provide the same field for each of our licensable product types -- and lucky for us, Organic Groups provides a totally sweet wrapper on the Field API that lets us do this easily. Within our commerce_license_og_configure_product_types function, we do a bunch of logic to figure out which fields we need to create on which product types, and then creating the field is as simple as this code snippet:

<?php
  $og_field
= og_fields_info(OG_AUDIENCE_FIELD);
 
$og_field['field']['settings']['target_type'] = $product_types[$new_bundle]['entity_type'];
 
$og_field['instance']['label'] = t('Licensed groups');
 
$og_field['instance']['settings']['behaviors']['og_widget']['admin']['widget_type'] = 'options_buttons';
 
 
og_create_field($field_name, 'commerce_product', $new_bundle, $og_field);
?>

This will give us an Organic Groups selection field on each licenseable product so that any given product can grant access to one or more groups on a configured-per-product basis.

Let's review. So far we have:

  1. Set up an adminstrative UI to enable specific product types for OG licenses.
  2. Added extra settings -- The group type we want them to refer to -- for each enabled product type.
  3. Used those extra settings to create an OG reference field on the product type, allowing each product to license one or more Organic Groups.

Writing our License Plugin

Now we have to use this information -- the Organic Groups referenced by a particular product entity -- to write the code that governs our license plugin. Let's go take a look at CommerceLicenseOg.class.php. Because our license is relatively simple, most of our methods are also simple or can inherit from the parent base class. Our license entity itself doesn't have any fields. The first key function is the accessDetails method that we use to print a link to each of the organic groups that this license's product grants access to:

<?php
 
public function accessDetails() {
   
// Create a set of links for each licensed OG.
   
$settings = commerce_license_og_product_types($this->wrapper->product->getBundle());
   
$field_name = commerce_license_og_field_name($settings);   

   
$links = '';
    foreach (
$this->wrapper->product->{$field_name} as $group_wrapper) {
     
$links .= l($group_wrapper->label(), entity_uri($group_wrapper->entityType(), $group_wrapper->value())) . "<br/>";
    }
    return
$links;
  }
?>

We use this in the checkoutCompletionMessage function to notify the user of their newfound group memberships.

The other key piece we need to override is the save method. This is where the magic happens -- in the save method, we will need to look at the license's product and which organic groups were chosen in that product's configuration. Based on that, we need to add or remove memberships from the user account which owns the license. Here is the setup code of the method:

<?php
 
public function save() {
    if (
$this->uid && $this->product_id) {
     
$settings = commerce_license_og_product_types($this->wrapper->product->getBundle());
     
$field_name = commerce_license_og_field_name($settings);
     
     
$group_wrappers = iterator_to_array($this->wrapper->product->{$field_name});
     
$owner = $this->wrapper->owner->value();
     
     
// Assume no change is necessary, yet.
     
$save_owner = FALSE;     
?>

Granting Access to each Organic Group

Now we have three pieces we need to tackle. First of all, there is a possibility that someone has changed this license entity's product. This isn't possible through the default UI, but it is possible through custom VBO actions or other code, and we need to account for the possibility. If the license's product ID has changed, then we remove all of the group memberships that were granted by the previous product:

<?php
     
if (!empty($this->license_id)) {
       
$this->original = entity_load_unchanged('commerce_license', $this->license_id);
       
// A plan change occurred. Remove previous group memberships
       
if ($this->original->product_id && $this->product_id != $this->original->product_id) {
         
$original_settings = commerce_license_og_product_types($this->original->wrapper->product->getBundle());
         
$original_field_name = commerce_license_og_field_name($original_settings);
          foreach (
$this->original->wrapper->product->{$field_name} as $original_group_wrapper) {
           
// Remove each previous group membership.
            // @todo: Only do this if the new plan also lacks this group.
           
$updated = $this->removeGroupMembership($original_group_wrapper, $settings['entity_type'], $owner);
           
$save_owner = ($save_owner || $updated);
          }
        }
      }
?>

Once that's done, we can move on to evaluating the license's status. If the license is active, we need to grant group memberships, and if it is not active (which translates to having a status "greater than" the active status) then we need to remove the previously-existing memberships.

<?php
     
if ($this->status == COMMERCE_LICENSE_ACTIVE) {
        foreach (
$group_wrappers as $group_wrapper) {
         
$updated = $this->verifyGroupMembership($group_wrapper, $settings['entity_type'], $owner);
         
$save_owner = ($save_owner || $updated);
        }
      }
      elseif (
$this->status > COMMERCE_LICENSE_ACTIVE) {
        foreach (
$group_wrappers as $group_wrapper) {
         
$updated = $this->removeGroupMembership($group_wrapper, $settings['entity_type'], $owner);
         
$save_owner = ($save_owner || $updated);
        }       
      }
?>

In each of these cases we use an extra method on the license (verifyGroupMembership and removeGroupMembership) to check if the membership exists and then add or remove it using og_group and og_ungroup as necessary.

With all of that taken care of, we're done! The finished project (still in dev state!!) can be found on Drupal.org. Let's review the list of steps we needed to implement our very own Commerce License type:

  1. Set up an adminstrative UI to enable specific product types for OG licenses.
  2. Added extra settings -- The group type we want them to refer to -- for each enabled product type.
  3. Used those extra settings to create an OG reference field on the product type, allowing each product to license one or more Organic Groups.
  4. Set up our license plugin and associated class, extending CommerceLicenseBase.
  5. Grant access to each organic group on the license product using logic in the license's ->save method.

The Commerce License API allows us to combine custom logic in the license type plugin class with individual settings on a license product to grant access to specific resources on a product-by-product basis. Combining this with an existing API from Drupal contributed modules (such as Organic Groups) makes it easy to manage access to nearly anything in the Drupal contrib space using license products as part of your Drupal Commerce store! We hope this has been a good walkthrough of the process for doing this with your own contributed module or your own resources, and we hope Commerce License can be a great tool to allow anyone to integrate their own content with a great e-commerce platform in Drupal Commerce!

Tags: 
Posted: Aug 26, 2014