Drupal Commerce Nuts & Bolts
Barcelona Drupal developer days, June 2012
About me
- Developer & trainer at Commerce Guys.
- Drupal community enthusiastic.
- Organizer of many Drupal local events.
- @pcambra
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
Thanks!
'