Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

For the best experience please use the latest Chrome, Safari or Firefox browser.

Drupal Commerce Nuts & Bolts

Barcelona Drupal developer days, June 2012

About me

Drupal Commerce is
an open source e-commerce framework really tightly integrated with Drupal 7. It lets you quickly create tailor-made e-commerce solutions allowing you to easily extend any aspect of the system.

Our vision is for Drupal Commerce to be the number one open source eCommerce platform in the world…
Powering truly flexible commerce

Leveraging core systems

Commerce depends heavily on the core fieldable entity system in Drupal 7.

Entity system

Define own entities for describing content: product types, line items, orders...

/**
 * Implements hook_entity_info().
 */
function commerce_product_entity_info() {
  $return = array(
    'commerce_product' => array(
      'label' => t('Commerce Product'),
      'controller class' => 'CommerceProductEntityController',
      'base table' => 'commerce_product',
      'revision table' => 'commerce_product_revision',
      'fieldable' => TRUE,
      'entity keys' => array(
        'id' => 'product_id',
        'bundle' => 'type',
        'label' => 'title',
        'revision' => 'revision_id',
      ),
      'bundle keys' => array(
        'bundle' => 'type',
      ),
      'bundles' => array(),
      'load hook' => 'commerce_product_load',
      'view modes' => array(
        'full' => array(
          'label' => t('Admin display'),
          'custom settings' => FALSE,
        ),
      ),
      (...)

reference

Fieldable entities

Building product types including "attribute" fields

/**
 * Implements hook_field_info().
 */
function commerce_price_field_info() {
  return array(
    'commerce_price' => array(
      'label' => t('Price'),
      'description' => t('This field stores prices for products...'),
      'settings' => array(),
      'instance_settings' => array(),
      'default_widget' => 'commerce_price_simple',
      'default_formatter' => 'commerce_price_default',
      'property_type' => 'commerce_price',
      'property_callbacks' => array('commerce_price_property_info_callback'),
      'default_token_formatter' => 'commerce_price_formatted_amount'
    ),
  );
}

reference

EntityFieldQuery

Query your entities and their field data without writing SQL or knowing schemas.

/**
 * Implements hook_commerce_product_can_delete().
 */
function commerce_product_reference_commerce_product_can_delete($product) {
  // Use EntityFieldQuery to look for line items referencing this product and do
  // not allow the delete to occur if one exists.
  $query = new EntityFieldQuery();

  $query
    ->entityCondition('entity_type', 'commerce_line_item', '=')
    ->entityCondition('bundle', commerce_product_line_item_types(), 'IN')
    ->fieldCondition('commerce_product', 'product_id', $product->product_id, '=')
    ->count();

  return $query->execute() == 0;
}

reference

Leveraging contributed systems

Commerce depends heavily on Rules, the contributed Entity API, and Views.

Rules

Configuring all sorts of conditional behavior.

  $events['commerce_cart_product_add'] = array(
    'label' => t('After adding a product to the cart'),
    'group' => t('Commerce Cart'),
    'variables' => commerce_cart_rules_event_variables(TRUE),
    'access callback' => 'commerce_order_rules_access',
  );

reference

Entity API

Use the entity metadata wrapper to easily access and manipulate field data and referenced entities on Commerce entities.

function commerce_cart_order_convert($order, $account) {
  // Only convert orders that are currently anonmyous orders.
  if ($order->uid == 0) {
    // Update the uid and e-mail address to match the current account since
    // there currently is no way to specify a custom e-mail address per order.
    $order->uid = $account->uid;
    $order->mail = $account->mail;

    // Update the uid of any referenced customer profiles.
    $order_wrapper = entity_metadata_wrapper('commerce_order', $order);

    foreach (commerce_customer_profile_types() as $type => $profile_type) {
      $field_name = 'commerce_customer_' . $type;

      if (!is_null($order_wrapper->{$field_name}->value()) &&
        $order_wrapper->{$field_name}->uid->value() == 0) {
        $order_wrapper->{$field_name}->uid = $account->uid;
        $order_wrapper->{$field_name}->save();
      }
    }
    // Allow other modules to operate on the converted order and then save.
    module_invoke_all('commerce_cart_order_convert', $order_wrapper, $account);
    $order_wrapper->save();
    return $order_wrapper;
  }

  return FALSE;
}

reference

Views

Administrative Views for all entities on the back end and the Cart on the front.

Views can now include area handlers and be used to build forms.

function views_form(&$form, &$form_state) {
    // The view is empty, abort.
    if (empty($this->view->result)) {
      return;
    }

    $form[$this->options['id']] = array(
      '#tree' => TRUE,
    );
    // At this point, the query has already been run, so we can access the results
    // in order to get the base key value (for example, nid for nodes).
    foreach ($this->view->result as $row_id => $row) {
      $line_item_id = $this->get_value($row);

      $form[$this->options['id']][$row_id] = array(
        '#type' => 'submit',
        '#value' => t('Delete'),
        '#name' => 'delete-line-item-' . $row_id,
        '#attributes' => array('class' => array('delete-line-item')),
        '#line_item_id' => $line_item_id,
        '#submit' => array_merge($form['#submit'], 
          array('commerce_line_item_line_item_views_delete_form_submit')),
      );
    }
  }

reference

Core commerce system

Commerce defines its own set of systems to interact with its entities.

Product vs. Product display

Products do not have default displays, but there are multiple ways for you to build custom displays: node + product reference, Views, Panels.

Product pricing

Product sell prices are calculated through Rules via pseudo line items.

function commerce_product_calculate_sell_price($product, $precalc = FALSE) {
  // First create a pseudo product line item that we will pass to Rules.
  $line_item = commerce_product_line_item_new($product);
  // Allow modules to prepare this as necessary.
  drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
(..)

  // Pass the line item to Rules.
  rules_invoke_event('commerce_product_calculate_sell_price', $line_item);

  return entity_metadata_wrapper('commerce_line_item', $line_item)
    ->commerce_unit_price->value();
        

reference

Price components

Price calculation builds an array of price components into a price field’s data array.

function commerce_tax_rate_calculate($tax_rate, $line_item_wrapper) {
  // By default, do not duplicate a tax that's already on the line item.
  if (!is_null($line_item_wrapper->commerce_unit_price->value()) &&
    !commerce_price_component_load($line_item_wrapper->commerce_unit_price->value(), 
      $tax_rate['price_component'])) {
    // Calculate the tax amount.
    $amount = $line_item_wrapper->commerce_unit_price->amount->value() * $tax_rate['rate'];

    return array(
      'amount' => commerce_tax_rate_round_amount($tax_rate, $amount),
      'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code->value(),
      'data' => array(
        'tax_rate' => $tax_rate,
      ),
    );
  }

  return FALSE;
}
     

reference

Cart

Shopping carts are orders with special handling for refreshing prices / checkout.

function commerce_cart_product_add($uid, $line_item, $combine = TRUE) {
  // Do not add the line item if it doesn't have a unit price.
  $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);

  if (is_null($line_item_wrapper->commerce_unit_price->value())) {
    return FALSE;
  }
  // First attempt to load the customer's shopping cart order.
  $order = commerce_cart_order_load($uid);

  // If no order existed, create one now.
  if (empty($order)) {
    $order = commerce_cart_order_new($uid);
  }

  // Set the incoming line item's order_id.
  $line_item->order_id = $order->order_id;

  // Wrap the order for easy access to field data.
  $order_wrapper = entity_metadata_wrapper('commerce_order', $order);

  // Extract the product and quantity we're adding from the incoming line item.
  $product = $line_item_wrapper->commerce_product->value();
  $quantity = $line_item->quantity;

  // Invoke the product prepare event with the shopping cart order.
  rules_invoke_all('commerce_cart_product_prepare', $order, 
    $product, $line_item->quantity);      
      

reference

Checkout

The form is highly configurable and updates the order upon each submission. Modules can define checkout panes for the drag-and-drop form builder.

function commerce_order_account_pane_checkout_form($form, &$form_state, $checkout_pane, 
  $order) {
  global $user;

  $pane_form = array();

  // If the user is logged in...
  if ($user->uid > 0) {
    // And the pane has been configured to display account information...
    if (variable_get('commerce_order_account_pane_auth_display', FALSE)) {
      // Note we're not using theme_username() to avoid linking out of checkout.
      $pane_form['username'] = array(
        '#type' => 'item',
        '#title' => t('Username'),
        '#markup' => check_plain($user->name),
      );
      $pane_form['mail'] = array(
        '#type' => 'item',
        '#title' => t('E-mail address'),
        '#markup' => check_plain($order->mail),
      );        
        

reference

Payment

Every service defined by a payment gateway is defined as a payment method. Each method can be instantiated any number of times with different API credentials, transaction settings, and conditional availability.

     
function commerce_payment_example_commerce_payment_method_info() {
  $payment_methods = array();

  $payment_methods['commerce_payment_example'] = array(
    'title' => t('Example payment'),
    'description' => t('Demonstrates complete payment during checkout and 
      serves as a development example.'),
    'active' => TRUE,
  );

  return $payment_methods;
}                     
        

reference

Complex conditions

Commerce defines complex conditions that you can use in place of chaining various Rules and Rules components together yourself.

Extended entity controllers

Commerce now defines a default entity controller that all its entities use. Contributed modules can extend the same controller for their entities.

      
class DrupalCommerceEntityController 
  extends DrupalDefaultEntityController 
    implements EntityAPIControllerInterface {
(...)
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
    $query = parent::buildQuery($ids, $conditions, $revision_id);

    if (isset($this->entityInfo['locking mode']) && 
      $this->entityInfo['locking mode'] == 'pessimistic') {
      // In pessimistic locking mode, we issue the load query with a FOR UPDATE
      // clause. This will block all other load queries to the loaded objects
      // but requires us to start a transaction.
      if (empty($this->controllerTransaction)) {
        $this->controllerTransaction = db_transaction();
      }

      $query->forUpdate();

      // Store the ids of the entities in the lockedEntities array for later
      // tracking, flipped for easier management via unset() below.
      if (is_array($ids)) {
        $this->lockedEntities += array_flip($ids);
      }
    }

    return $query;
  }        
        

reference

Generic entity access

Commerce now defines a generic set of entity access permissions and an access callback function that entities can use to perform access checks. Entity view access is extensible through hook_query_TAG_alter().

  
function commerce_payment_query_commerce_payment_transaction_access_alter
  (QueryAlterableInterface $query) {
  // Read the meta-data from the query.
  if (!$account = $query->getMetaData('account')) {
    global $user;
    $account = $user;
  }
  // If the user has the administration permission, nothing to do.
  if (user_access('administer payments', $account)) {
    return;
  }

  // Join the payment transaction to their orders.
  if (user_access('view payments', $account)) {
    $tables = &$query->getTables();
    $base_table = key($tables);
    $order_alias = $query->innerJoin('commerce_order', 'co', 
      '%alias.order_id = ' . $base_table . '.order_id');

    // Perform the access control on the order.
    commerce_entity_access_query_alter($query, 'commerce_order', $order_alias);
  }
  else {
    // The user has access to no payment transaction.
    $query->where('1 = 0');
  }
}
        

reference

reference

Credits


Ryan Szrama at Drupalcamp Colorado 2011

Julien Dubois at Drupalcamp Spain 2011


Bojan Zivanovic at Drupal Commerce camp in Lucerne, 2011

Resources

Credit

Thanks!

'